diff --git a/auth/access-control.go b/auth/access-control.go
index d984c9d4..a726a4ce 100644
--- a/auth/access-control.go
+++ b/auth/access-control.go
@@ -155,18 +155,6 @@ func VerifyPublicAccess(ctx context.Context, be backend.Backend, action Action,
return nil
}
-func MayCreateBucket(acct Account, isRoot bool) error {
- if isRoot {
- return nil
- }
-
- if acct.Role == RoleUser {
- return s3err.GetAPIError(s3err.ErrAccessDenied)
- }
-
- return nil
-}
-
func IsAdminOrOwner(acct Account, isRoot bool, acl ACL) error {
// Owner check
if acct.Access == acl.Owner {
diff --git a/auth/acl.go b/auth/acl.go
index e9003243..79ac6e18 100644
--- a/auth/acl.go
+++ b/auth/acl.go
@@ -385,7 +385,7 @@ func CheckIfAccountsExist(accs []string, iam IAMService) ([]string, error) {
for _, acc := range accs {
_, err := iam.GetUserAccount(acc)
if err != nil {
- if err == ErrNoSuchUser {
+ if err == ErrNoSuchUser || err == s3err.GetAPIError(s3err.ErrAdminUserNotFound) {
result = append(result, acc)
continue
}
diff --git a/auth/bucket_policy.go b/auth/bucket_policy.go
index 4f817199..6d6d1495 100644
--- a/auth/bucket_policy.go
+++ b/auth/bucket_policy.go
@@ -17,6 +17,7 @@ package auth
import (
"encoding/json"
"errors"
+ "fmt"
"net/http"
"github.com/versity/versitygw/s3err"
@@ -194,7 +195,7 @@ func ValidatePolicyDocument(policyBin []byte, bucket string, iam IAMService) err
func VerifyBucketPolicy(policy []byte, access, bucket, object string, action Action) error {
var bucketPolicy BucketPolicy
if err := json.Unmarshal(policy, &bucketPolicy); err != nil {
- return err
+ return fmt.Errorf("failed to parse the bucket policy: %w", err)
}
resource := bucket
diff --git a/backend/common.go b/backend/common.go
index 951d4841..7aaa80b0 100644
--- a/backend/common.go
+++ b/backend/common.go
@@ -22,6 +22,7 @@ import (
"hash"
"io"
"io/fs"
+ "math"
"net/url"
"os"
"regexp"
@@ -113,7 +114,10 @@ func ParseObjectRange(size int64, acceptRange string) (int64, int64, bool, error
return 0, size, false, nil
}
- startOffset, err := strconv.ParseInt(bRange[0], 10, 64)
+ startOffset, err := strconv.ParseInt(bRange[0], 10, strconv.IntSize)
+ if startOffset > int64(math.MaxInt) || startOffset < int64(math.MinInt) {
+ return 0, size, false, errInvalidRange
+ }
if err != nil && bRange[0] != "" {
return 0, size, false, nil
}
@@ -128,7 +132,10 @@ func ParseObjectRange(size int64, acceptRange string) (int64, int64, bool, error
return startOffset, size - startOffset, true, nil
}
- endOffset, err := strconv.ParseInt(bRange[1], 10, 64)
+ endOffset, err := strconv.ParseInt(bRange[1], 10, strconv.IntSize)
+ if endOffset > int64(math.MaxInt) {
+ return 0, size, false, errInvalidRange
+ }
if err != nil {
return 0, size, false, nil
}
diff --git a/backend/posix/posix.go b/backend/posix/posix.go
index d8660a23..c71874b3 100644
--- a/backend/posix/posix.go
+++ b/backend/posix/posix.go
@@ -2419,6 +2419,10 @@ func (p *Posix) UploadPart(ctx context.Context, input *s3.UploadPartInput) (*s3.
if errors.Is(err, syscall.EDQUOT) {
return nil, s3err.GetAPIError(s3err.ErrQuotaExceeded)
}
+ // Return the error itself, if it's an 's3err.APIError'
+ if _, ok := err.(s3err.APIError); ok {
+ return nil, err
+ }
return nil, fmt.Errorf("write part data: %w", err)
}
@@ -2853,6 +2857,10 @@ func (p *Posix) PutObject(ctx context.Context, po s3response.PutObjectInput) (s3
if errors.Is(err, syscall.EDQUOT) {
return s3response.PutObjectOutput{}, s3err.GetAPIError(s3err.ErrQuotaExceeded)
}
+ // Return the error itself, if it's an 's3err.APIError'
+ if _, ok := err.(s3err.APIError); ok {
+ return s3response.PutObjectOutput{}, err
+ }
return s3response.PutObjectOutput{}, fmt.Errorf("write object data: %w", err)
}
diff --git a/cmd/versitygw/main.go b/cmd/versitygw/main.go
index 8ff4e620..f5b84efe 100644
--- a/cmd/versitygw/main.go
+++ b/cmd/versitygw/main.go
@@ -621,30 +621,6 @@ func runGateway(ctx context.Context, be backend.Backend) error {
opts = append(opts, s3api.WithHostStyle(virtualDomain))
}
- admApp := fiber.New(fiber.Config{
- AppName: "versitygw",
- ServerHeader: "VERSITYGW",
- Network: fiber.NetworkTCP,
- DisableStartupMessage: true,
- })
-
- var admOpts []s3api.AdminOpt
-
- if admCertFile != "" || admKeyFile != "" {
- if admCertFile == "" {
- return fmt.Errorf("TLS key specified without cert file")
- }
- if admKeyFile == "" {
- return fmt.Errorf("TLS cert specified without key file")
- }
-
- cert, err := tls.LoadX509KeyPair(admCertFile, admKeyFile)
- if err != nil {
- return fmt.Errorf("tls: load certs: %v", err)
- }
- admOpts = append(admOpts, s3api.WithAdminSrvTLS(cert))
- }
-
iam, err := auth.New(&auth.Opts{
RootAccount: auth.Account{
Access: rootUserAccess,
@@ -732,7 +708,41 @@ func runGateway(ctx context.Context, be backend.Backend) error {
return fmt.Errorf("init gateway: %v", err)
}
- admSrv := s3api.NewAdminServer(admApp, be, middlewares.RootUserConfig{Access: rootUserAccess, Secret: rootUserSecret}, admPort, region, iam, loggers.AdminLogger, admOpts...)
+ var admSrv *s3api.S3AdminServer
+
+ if admPort != "" {
+ admApp := fiber.New(fiber.Config{
+ AppName: "versitygw",
+ ServerHeader: "VERSITYGW",
+ Network: fiber.NetworkTCP,
+ DisableStartupMessage: true,
+ })
+
+ var opts []s3api.AdminOpt
+
+ if admCertFile != "" || admKeyFile != "" {
+ if admCertFile == "" {
+ return fmt.Errorf("TLS key specified without cert file")
+ }
+ if admKeyFile == "" {
+ return fmt.Errorf("TLS cert specified without key file")
+ }
+
+ cert, err := tls.LoadX509KeyPair(admCertFile, admKeyFile)
+ if err != nil {
+ return fmt.Errorf("tls: load certs: %v", err)
+ }
+ opts = append(opts, s3api.WithAdminSrvTLS(cert))
+ }
+ if quiet {
+ opts = append(opts, s3api.WithAdminQuiet())
+ }
+ if debug {
+ opts = append(opts, s3api.WithAdminDebug())
+ }
+
+ admSrv = s3api.NewAdminServer(admApp, be, middlewares.RootUserConfig{Access: rootUserAccess, Secret: rootUserSecret}, admPort, region, iam, loggers.AdminLogger, opts...)
+ }
if !quiet {
printBanner(port, admPort, certFile != "", admCertFile != "")
@@ -977,10 +987,7 @@ func getMatchingIPs(spec string) ([]string, error) {
const columnWidth = 70
func centerText(text string) string {
- padding := (columnWidth - 2 - len(text)) / 2
- if padding < 0 {
- padding = 0
- }
+ padding := max((columnWidth-2-len(text))/2, 0)
return strings.Repeat(" ", padding) + text
}
diff --git a/go.mod b/go.mod
index e70ce217..bb54d8f5 100644
--- a/go.mod
+++ b/go.mod
@@ -22,6 +22,7 @@ require (
github.com/pkg/xattr v0.4.12
github.com/segmentio/kafka-go v0.4.48
github.com/smira/go-statsd v1.3.4
+ github.com/stretchr/testify v1.10.0
github.com/urfave/cli/v2 v2.27.7
github.com/valyala/fasthttp v1.64.0
github.com/versity/scoutfs-go v0.0.0-20240325223134-38eb2f5f7d44
@@ -39,6 +40,7 @@ require (
github.com/aws/aws-sdk-go-v2/service/sso v1.25.6 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.4 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.34.1 // indirect
+ github.com/davecgh/go-spew v1.1.1 // indirect
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 // indirect
github.com/golang-jwt/jwt/v5 v5.2.3 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
@@ -51,11 +53,13 @@ require (
github.com/nats-io/nuid v1.0.1 // indirect
github.com/pierrec/lz4/v4 v4.1.22 // indirect
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
+ github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/ryanuber/go-glob v1.0.0 // indirect
golang.org/x/crypto v0.40.0 // indirect
golang.org/x/net v0.42.0 // indirect
golang.org/x/text v0.27.0 // indirect
golang.org/x/time v0.12.0 // indirect
+ gopkg.in/yaml.v3 v3.0.1 // indirect
)
require (
diff --git a/go.sum b/go.sum
index ee78aeb7..2aa7aeea 100644
--- a/go.sum
+++ b/go.sum
@@ -118,6 +118,10 @@ github.com/keybase/go-keychain v0.0.1/go.mod h1:PdEILRW3i9D8JcdM+FmY6RwkHGnhHxXw
github.com/klauspost/compress v1.15.9/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
+github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
+github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
+github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
+github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
@@ -152,6 +156,8 @@ github.com/redis/go-redis/v9 v9.8.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
+github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
+github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk=
@@ -163,8 +169,9 @@ github.com/smira/go-statsd v1.3.4 h1:kBYWcLSGT+qC6JVbvfz48kX7mQys32fjDOPrfmsSx2c
github.com/smira/go-statsd v1.3.4/go.mod h1:RjdsESPgDODtg1VpVVf9MJrEW2Hw0wtRNbmB1CAhu6A=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
-github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
+github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
+github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
@@ -259,6 +266,8 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/metrics/metrics.go b/metrics/metrics.go
index 22cad81e..753b4057 100644
--- a/metrics/metrics.go
+++ b/metrics/metrics.go
@@ -41,8 +41,14 @@ type Tag struct {
Value string
}
-// Manager is a manager of metrics plugins
-type Manager struct {
+// Manager is the interface definition for metrics manager
+type Manager interface {
+ Send(ctx *fiber.Ctx, err error, action string, count int64, status int)
+ Close()
+}
+
+// manager is a manager of metrics plugins
+type manager struct {
wg sync.WaitGroup
ctx context.Context
@@ -59,7 +65,7 @@ type Config struct {
}
// NewManager initializes metrics plugins and returns a new metrics manager
-func NewManager(ctx context.Context, conf Config) (*Manager, error) {
+func NewManager(ctx context.Context, conf Config) (Manager, error) {
if len(conf.StatsdServers) == 0 && len(conf.DogStatsdServers) == 0 {
return nil, nil
}
@@ -74,7 +80,7 @@ func NewManager(ctx context.Context, conf Config) (*Manager, error) {
addDataChan := make(chan datapoint, dataItemCount)
- mgr := &Manager{
+ mgr := &manager{
addDataChan: addDataChan,
ctx: ctx,
config: conf,
@@ -112,7 +118,7 @@ func NewManager(ctx context.Context, conf Config) (*Manager, error) {
return mgr, nil
}
-func (m *Manager) Send(ctx *fiber.Ctx, err error, action string, count int64, status int) {
+func (m *manager) Send(ctx *fiber.Ctx, err error, action string, count int64, status int) {
// In case of Authentication failures, url parsing ...
if action == "" {
action = ActionUndetected
@@ -168,12 +174,12 @@ func (m *Manager) Send(ctx *fiber.Ctx, err error, action string, count int64, st
}
// increment increments the key by one
-func (m *Manager) increment(key string, tags ...Tag) {
+func (m *manager) increment(key string, tags ...Tag) {
m.add(key, 1, tags...)
}
// add adds value to key
-func (m *Manager) add(key string, value int64, tags ...Tag) {
+func (m *manager) add(key string, value int64, tags ...Tag) {
if m.ctx.Err() != nil {
return
}
@@ -192,7 +198,7 @@ func (m *Manager) add(key string, value int64, tags ...Tag) {
}
// Close closes metrics channels, waits for data to complete, closes all plugins
-func (m *Manager) Close() {
+func (m *manager) Close() {
// drain the datapoint channels
close(m.addDataChan)
m.wg.Wait()
@@ -209,7 +215,7 @@ type publisher interface {
Close()
}
-func (m *Manager) addForwarder(addChan <-chan datapoint) {
+func (m *manager) addForwarder(addChan <-chan datapoint) {
for data := range addChan {
for _, s := range m.publishers {
s.Add(data.key, data.value, data.tags...)
diff --git a/s3api/admin-router.go b/s3api/admin-router.go
index 65ec084a..ca10b639 100644
--- a/s3api/admin-router.go
+++ b/s3api/admin-router.go
@@ -18,30 +18,59 @@ import (
"github.com/gofiber/fiber/v2"
"github.com/versity/versitygw/auth"
"github.com/versity/versitygw/backend"
+ "github.com/versity/versitygw/metrics"
"github.com/versity/versitygw/s3api/controllers"
+ "github.com/versity/versitygw/s3api/middlewares"
"github.com/versity/versitygw/s3log"
)
type S3AdminRouter struct{}
-func (ar *S3AdminRouter) Init(app *fiber.App, be backend.Backend, iam auth.IAMService, logger s3log.AuditLogger) {
- controller := controllers.NewAdminController(iam, be, logger)
+func (ar *S3AdminRouter) Init(app *fiber.App, be backend.Backend, iam auth.IAMService, logger s3log.AuditLogger, root middlewares.RootUserConfig, region string, debug bool) {
+ ctrl := controllers.NewAdminController(iam, be, logger)
+ services := &controllers.Services{
+ Logger: logger,
+ }
// CreateUser admin api
- app.Patch("/create-user", controller.CreateUser)
+ app.Patch("/create-user",
+ controllers.ProcessHandlers(ctrl.CreateUser, metrics.ActionAdminCreateUser, services,
+ middlewares.VerifyV4Signature(root, iam, region, debug),
+ middlewares.IsAdmin(metrics.ActionAdminCreateUser),
+ ))
// DeleteUsers admin api
- app.Patch("/delete-user", controller.DeleteUser)
+ app.Patch("/delete-user",
+ controllers.ProcessHandlers(ctrl.DeleteUser, metrics.ActionAdminDeleteUser, services,
+ middlewares.VerifyV4Signature(root, iam, region, debug),
+ middlewares.IsAdmin(metrics.ActionAdminDeleteUser),
+ ))
// UpdateUser admin api
- app.Patch("/update-user", controller.UpdateUser)
+ app.Patch("/update-user",
+ controllers.ProcessHandlers(ctrl.UpdateUser, metrics.ActionAdminUpdateUser, services,
+ middlewares.VerifyV4Signature(root, iam, region, debug),
+ middlewares.IsAdmin(metrics.ActionAdminUpdateUser),
+ ))
// ListUsers admin api
- app.Patch("/list-users", controller.ListUsers)
+ app.Patch("/list-users",
+ controllers.ProcessHandlers(ctrl.ListUsers, metrics.ActionAdminListUsers, services,
+ middlewares.VerifyV4Signature(root, iam, region, debug),
+ middlewares.IsAdmin(metrics.ActionAdminListUsers),
+ ))
// ChangeBucketOwner admin api
- app.Patch("/change-bucket-owner", controller.ChangeBucketOwner)
+ app.Patch("/change-bucket-owner",
+ controllers.ProcessHandlers(ctrl.ChangeBucketOwner, metrics.ActionAdminChangeBucketOwner, services,
+ middlewares.VerifyV4Signature(root, iam, region, debug),
+ middlewares.IsAdmin(metrics.ActionAdminChangeBucketOwner),
+ ))
// ListBucketsAndOwners admin api
- app.Patch("/list-buckets", controller.ListBuckets)
+ app.Patch("/list-buckets",
+ controllers.ProcessHandlers(ctrl.ListBuckets, metrics.ActionAdminListBuckets, services,
+ middlewares.VerifyV4Signature(root, iam, region, debug),
+ middlewares.IsAdmin(metrics.ActionAdminListBuckets),
+ ))
}
diff --git a/s3api/admin-server.go b/s3api/admin-server.go
index ee132d74..979f4793 100644
--- a/s3api/admin-server.go
+++ b/s3api/admin-server.go
@@ -21,6 +21,7 @@ import (
"github.com/gofiber/fiber/v2/middleware/logger"
"github.com/versity/versitygw/auth"
"github.com/versity/versitygw/backend"
+ "github.com/versity/versitygw/s3api/controllers"
"github.com/versity/versitygw/s3api/middlewares"
"github.com/versity/versitygw/s3log"
)
@@ -31,6 +32,8 @@ type S3AdminServer struct {
router *S3AdminRouter
port string
cert *tls.Certificate
+ quiet bool
+ debug bool
}
func NewAdminServer(app *fiber.App, be backend.Backend, root middlewares.RootUserConfig, port, region string, iam auth.IAMService, l s3log.AuditLogger, opts ...AdminOpt) *S3AdminServer {
@@ -46,17 +49,15 @@ func NewAdminServer(app *fiber.App, be backend.Backend, root middlewares.RootUse
}
// Logging middlewares
- app.Use(logger.New())
- app.Use(middlewares.DecodeURL(l, nil))
+ if !server.quiet {
+ app.Use(logger.New(logger.Config{
+ Format: "${time} | ${status} | ${latency} | ${ip} | ${method} | ${path} | ${error} | ${queryParams}\n",
+ }))
+ }
+ app.Use(controllers.WrapMiddleware(middlewares.DecodeURL, l, nil))
+ app.Use(middlewares.DebugLogger())
- // Authentication middlewares
- app.Use(middlewares.VerifyV4Signature(root, iam, l, nil, region, false))
- app.Use(middlewares.VerifyMD5Body(l))
-
- // Admin role checker
- app.Use(middlewares.IsAdmin(l))
-
- server.router.Init(app, be, iam, l)
+ server.router.Init(app, be, iam, l, root, region, server.debug)
return server
}
@@ -67,6 +68,16 @@ func WithAdminSrvTLS(cert tls.Certificate) AdminOpt {
return func(s *S3AdminServer) { s.cert = &cert }
}
+// WithQuiet silences default logging output
+func WithAdminQuiet() AdminOpt {
+ return func(s *S3AdminServer) { s.quiet = true }
+}
+
+// WithAdminDebug enables the debug logging
+func WithAdminDebug() AdminOpt {
+ return func(s *S3AdminServer) { s.debug = true }
+}
+
func (sa *S3AdminServer) Serve() (err error) {
if sa.cert != nil {
return sa.app.ListenTLSWithCertificate(sa.port, *sa.cert)
diff --git a/s3api/controllers/admin.go b/s3api/controllers/admin.go
index d10dae18..a85caf0e 100644
--- a/s3api/controllers/admin.go
+++ b/s3api/controllers/admin.go
@@ -22,7 +22,6 @@ import (
"github.com/gofiber/fiber/v2"
"github.com/versity/versitygw/auth"
"github.com/versity/versitygw/backend"
- "github.com/versity/versitygw/metrics"
"github.com/versity/versitygw/s3err"
"github.com/versity/versitygw/s3log"
"github.com/versity/versitygw/s3response"
@@ -38,23 +37,19 @@ func NewAdminController(iam auth.IAMService, be backend.Backend, l s3log.AuditLo
return AdminController{iam: iam, be: be, l: l}
}
-func (c AdminController) CreateUser(ctx *fiber.Ctx) error {
+func (c AdminController) CreateUser(ctx *fiber.Ctx) (*Response, error) {
var usr auth.Account
err := xml.Unmarshal(ctx.Body(), &usr)
if err != nil {
- return SendResponse(ctx, s3err.GetAPIError(s3err.ErrMalformedXML),
- &MetaOpts{
- Logger: c.l,
- Action: metrics.ActionAdminCreateUser,
- })
+ return &Response{
+ MetaOpts: &MetaOptions{},
+ }, s3err.GetAPIError(s3err.ErrMalformedXML)
}
if !usr.Role.IsValid() {
- return SendResponse(ctx, s3err.GetAPIError(s3err.ErrAdminInvalidUserRole),
- &MetaOpts{
- Logger: c.l,
- Action: metrics.ActionAdminCreateUser,
- })
+ return &Response{
+ MetaOpts: &MetaOptions{},
+ }, s3err.GetAPIError(s3err.ErrAdminInvalidUserRole)
}
err = c.iam.CreateAccount(usr)
@@ -63,47 +58,38 @@ func (c AdminController) CreateUser(ctx *fiber.Ctx) error {
err = s3err.GetAPIError(s3err.ErrAdminUserExists)
}
- return SendResponse(ctx, err,
- &MetaOpts{
- Logger: c.l,
- Action: metrics.ActionAdminCreateUser,
- })
+ return &Response{
+ MetaOpts: &MetaOptions{},
+ }, err
}
- return SendResponse(ctx, nil,
- &MetaOpts{
- Logger: c.l,
- Action: metrics.ActionAdminCreateUser,
+ return &Response{
+ MetaOpts: &MetaOptions{
Status: http.StatusCreated,
- })
+ },
+ }, nil
}
-func (c AdminController) UpdateUser(ctx *fiber.Ctx) error {
+func (c AdminController) UpdateUser(ctx *fiber.Ctx) (*Response, error) {
access := ctx.Query("access")
if access == "" {
- return SendResponse(ctx, s3err.GetAPIError(s3err.ErrAdminMissingUserAcess),
- &MetaOpts{
- Logger: c.l,
- Action: metrics.ActionAdminUpdateUser,
- })
+ return &Response{
+ MetaOpts: &MetaOptions{},
+ }, s3err.GetAPIError(s3err.ErrAdminMissingUserAcess)
}
var props auth.MutableProps
if err := xml.Unmarshal(ctx.Body(), &props); err != nil {
- return SendResponse(ctx, s3err.GetAPIError(s3err.ErrMalformedXML),
- &MetaOpts{
- Logger: c.l,
- Action: metrics.ActionAdminUpdateUser,
- })
+ return &Response{
+ MetaOpts: &MetaOptions{},
+ }, s3err.GetAPIError(s3err.ErrMalformedXML)
}
err := props.Validate()
if err != nil {
- return SendResponse(ctx, s3err.GetAPIError(s3err.ErrAdminInvalidUserRole),
- &MetaOpts{
- Logger: c.l,
- Action: metrics.ActionAdminUpdateUser,
- })
+ return &Response{
+ MetaOpts: &MetaOptions{},
+ }, s3err.GetAPIError(s3err.ErrAdminInvalidUserRole)
}
err = c.iam.UpdateUserAccount(access, props)
@@ -112,78 +98,66 @@ func (c AdminController) UpdateUser(ctx *fiber.Ctx) error {
err = s3err.GetAPIError(s3err.ErrAdminUserNotFound)
}
- return SendResponse(ctx, err,
- &MetaOpts{
- Logger: c.l,
- Action: metrics.ActionAdminUpdateUser,
- })
+ return &Response{
+ MetaOpts: &MetaOptions{},
+ }, err
}
- return SendResponse(ctx, nil,
- &MetaOpts{
- Logger: c.l,
- Action: metrics.ActionAdminUpdateUser,
- })
+ return &Response{
+ MetaOpts: &MetaOptions{},
+ }, nil
}
-func (c AdminController) DeleteUser(ctx *fiber.Ctx) error {
+func (c AdminController) DeleteUser(ctx *fiber.Ctx) (*Response, error) {
access := ctx.Query("access")
+ if access == "" {
+ return &Response{
+ MetaOpts: &MetaOptions{},
+ }, s3err.GetAPIError(s3err.ErrAdminMissingUserAcess)
+ }
err := c.iam.DeleteUserAccount(access)
- return SendResponse(ctx, err,
- &MetaOpts{
- Logger: c.l,
- Action: metrics.ActionAdminDeleteUser,
- })
+ return &Response{
+ MetaOpts: &MetaOptions{},
+ }, err
}
-func (c AdminController) ListUsers(ctx *fiber.Ctx) error {
+func (c AdminController) ListUsers(ctx *fiber.Ctx) (*Response, error) {
accs, err := c.iam.ListUserAccounts()
- return SendXMLResponse(ctx,
- auth.ListUserAccountsResult{
- Accounts: accs,
- }, err,
- &MetaOpts{
- Logger: c.l,
- Action: metrics.ActionAdminListUsers,
- })
+ return &Response{
+ Data: auth.ListUserAccountsResult{Accounts: accs},
+ MetaOpts: &MetaOptions{},
+ }, err
}
-func (c AdminController) ChangeBucketOwner(ctx *fiber.Ctx) error {
+func (c AdminController) ChangeBucketOwner(ctx *fiber.Ctx) (*Response, error) {
owner := ctx.Query("owner")
bucket := ctx.Query("bucket")
accs, err := auth.CheckIfAccountsExist([]string{owner}, c.iam)
if err != nil {
- return SendResponse(ctx, err,
- &MetaOpts{
- Logger: c.l,
- Action: metrics.ActionAdminChangeBucketOwner,
- })
+ return &Response{
+ MetaOpts: &MetaOptions{},
+ }, err
}
if len(accs) > 0 {
- return SendResponse(ctx, s3err.GetAPIError(s3err.ErrAdminUserNotFound),
- &MetaOpts{
- Logger: c.l,
- Action: metrics.ActionAdminChangeBucketOwner,
- })
+ return &Response{
+ MetaOpts: &MetaOptions{},
+ }, s3err.GetAPIError(s3err.ErrAdminUserNotFound)
}
err = c.be.ChangeBucketOwner(ctx.Context(), bucket, owner)
- return SendResponse(ctx, err,
- &MetaOpts{
- Logger: c.l,
- Action: metrics.ActionAdminChangeBucketOwner,
- })
+ return &Response{
+ MetaOpts: &MetaOptions{},
+ }, err
}
-func (c AdminController) ListBuckets(ctx *fiber.Ctx) error {
+func (c AdminController) ListBuckets(ctx *fiber.Ctx) (*Response, error) {
buckets, err := c.be.ListBucketsAndOwners(ctx.Context())
- return SendXMLResponse(ctx,
- s3response.ListBucketsResult{
+ return &Response{
+ Data: s3response.ListBucketsResult{
Buckets: buckets,
- }, err, &MetaOpts{
- Logger: c.l,
- Action: metrics.ActionAdminListBuckets,
- })
+ },
+ MetaOpts: &MetaOptions{},
+ }, err
}
diff --git a/s3api/controllers/admin_test.go b/s3api/controllers/admin_test.go
index aaa82a04..73ef184b 100644
--- a/s3api/controllers/admin_test.go
+++ b/s3api/controllers/admin_test.go
@@ -16,439 +16,564 @@ package controllers
import (
"context"
- "fmt"
+ "encoding/xml"
+ "errors"
"net/http"
- "net/http/httptest"
- "strings"
"testing"
- "github.com/gofiber/fiber/v2"
+ "github.com/stretchr/testify/assert"
"github.com/versity/versitygw/auth"
+ "github.com/versity/versitygw/backend"
+ "github.com/versity/versitygw/s3api/utils"
+ "github.com/versity/versitygw/s3err"
+ "github.com/versity/versitygw/s3log"
"github.com/versity/versitygw/s3response"
)
-func TestAdminController_CreateUser(t *testing.T) {
+func TestNewAdminController(t *testing.T) {
type args struct {
- req *http.Request
+ iam auth.IAMService
+ be backend.Backend
+ l s3log.AuditLogger
}
-
- adminController := AdminController{
- iam: &IAMServiceMock{
- CreateAccountFunc: func(account auth.Account) error {
- return nil
- },
- },
- }
-
- app := fiber.New()
-
- app.Patch("/create-user", adminController.CreateUser)
-
- succUser := `
-
- access
- secret
- admin
- 0
- 0
-
- `
- invuser := `
-
- access
- secret
- invalid_role
- 0
- 0
-
- `
-
tests := []struct {
- name string
- app *fiber.App
- args args
- wantErr bool
- statusCode int
+ name string
+ args args
+ want AdminController
}{
{
- name: "Admin-create-user-malformed-body",
- app: app,
- args: args{
- req: httptest.NewRequest(http.MethodPatch, "/create-user", nil),
- },
- wantErr: false,
- statusCode: 400,
- },
- {
- name: "Admin-create-user-invalid-requester-role",
- app: app,
- args: args{
- req: httptest.NewRequest(http.MethodPatch, "/create-user", strings.NewReader(invuser)),
- },
- wantErr: false,
- statusCode: 400,
- },
- {
- name: "Admin-create-user-success",
- app: app,
- args: args{
- req: httptest.NewRequest(http.MethodPatch, "/create-user", strings.NewReader(succUser)),
- },
- wantErr: false,
- statusCode: 201,
+ name: "initialize admin api",
+ args: args{},
+ want: AdminController{},
},
}
for _, tt := range tests {
- resp, err := tt.app.Test(tt.args.req)
+ t.Run(tt.name, func(t *testing.T) {
+ got := NewAdminController(tt.args.iam, tt.args.be, tt.args.l)
+ assert.Equal(t, got, tt.want)
+ })
+ }
+}
- if (err != nil) != tt.wantErr {
- t.Errorf("AdminController.CreateUser() error = %v, wantErr %v", err, tt.wantErr)
- }
+func TestAdminController_CreateUser(t *testing.T) {
+ validBody, err := xml.Marshal(auth.Account{
+ Access: "access",
+ Secret: "secret",
+ Role: auth.RoleAdmin,
+ })
+ assert.NoError(t, err)
- if resp.StatusCode != tt.statusCode {
- t.Errorf("AdminController.CreateUser() statusCode = %v, wantStatusCode = %v", resp.StatusCode, tt.statusCode)
- }
+ invalidUserRoleBody, err := xml.Marshal(auth.Account{
+ Access: "access",
+ Secret: "secret",
+ Role: auth.Role("invalid_role"),
+ })
+ assert.NoError(t, err)
+
+ tests := []struct {
+ name string
+ input testInput
+ output testOutput
+ }{
+ {
+ name: "invalid request body",
+ input: testInput{
+ body: []byte("invalid_request_body"),
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{},
+ },
+ err: s3err.GetAPIError(s3err.ErrMalformedXML),
+ },
+ },
+ {
+ name: "invalid user role",
+ input: testInput{
+ body: invalidUserRoleBody,
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{},
+ },
+ err: s3err.GetAPIError(s3err.ErrAdminInvalidUserRole),
+ },
+ },
+ {
+ name: "backend returns user exists error",
+ input: testInput{
+ body: validBody,
+ beErr: auth.ErrUserExists,
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{},
+ },
+ err: s3err.GetAPIError(s3err.ErrAdminUserExists),
+ },
+ },
+ {
+ name: "backend returns other error",
+ input: testInput{
+ body: validBody,
+ beErr: s3err.GetAPIError(s3err.ErrInvalidRequest),
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{},
+ },
+ err: s3err.GetAPIError(s3err.ErrInvalidRequest),
+ },
+ },
+ {
+ name: "successful response",
+ input: testInput{
+ body: validBody,
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{
+ Status: http.StatusCreated,
+ },
+ },
+ },
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ iam := &IAMServiceMock{
+ CreateAccountFunc: func(account auth.Account) error {
+ return tt.input.beErr
+ },
+ }
+
+ ctrl := AdminController{
+ iam: iam,
+ }
+
+ testController(
+ t,
+ ctrl.CreateUser,
+ tt.output.response,
+ tt.output.err,
+ ctxInputs{
+ body: tt.input.body,
+ })
+ })
}
}
func TestAdminController_UpdateUser(t *testing.T) {
- type args struct {
- req *http.Request
- }
+ validBody, err := xml.Marshal(auth.MutableProps{
+ Secret: utils.GetStringPtr("secret"),
+ Role: auth.RoleAdmin,
+ })
+ assert.NoError(t, err)
- adminController := AdminController{
- iam: &IAMServiceMock{
- UpdateUserAccountFunc: func(access string, props auth.MutableProps) error {
- return nil
- },
- },
- }
-
- app := fiber.New()
-
- app.Patch("/update-user", adminController.UpdateUser)
-
- adminControllerErr := AdminController{
- iam: &IAMServiceMock{
- UpdateUserAccountFunc: func(access string, props auth.MutableProps) error {
- return auth.ErrNoSuchUser
- },
- },
- }
-
- appNotFound := fiber.New()
-
- appNotFound.Patch("/update-user", adminControllerErr.UpdateUser)
-
- succUser := `
-
- secret
- 0
- 0
-
- `
+ invalidUserRoleBody, err := xml.Marshal(auth.MutableProps{
+ Secret: utils.GetStringPtr("secret"),
+ Role: auth.Role("invalid_role"),
+ })
+ assert.NoError(t, err)
tests := []struct {
- name string
- app *fiber.App
- args args
- wantErr bool
- statusCode int
+ name string
+ input testInput
+ output testOutput
}{
{
- name: "Admin-update-user-success",
- app: app,
- args: args{
- req: httptest.NewRequest(http.MethodPatch, "/update-user?access=access", strings.NewReader(succUser)),
+ name: "missing user access key",
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{},
+ },
+ err: s3err.GetAPIError(s3err.ErrAdminMissingUserAcess),
},
- wantErr: false,
- statusCode: 200,
},
{
- name: "Admin-update-user-missing-access",
- app: app,
- args: args{
- req: httptest.NewRequest(http.MethodPatch, "/update-user", strings.NewReader(succUser)),
+ name: "invalid request body",
+ input: testInput{
+ body: []byte("invalid_request_body"),
+ queries: map[string]string{
+ "access": "user",
+ },
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{},
+ },
+ err: s3err.GetAPIError(s3err.ErrMalformedXML),
},
- wantErr: false,
- statusCode: 404,
},
{
- name: "Admin-update-user-invalid-request-body",
- app: app,
- args: args{
- req: httptest.NewRequest(http.MethodPatch, "/update-user?access=access", nil),
+ name: "invalid user role",
+ input: testInput{
+ body: invalidUserRoleBody,
+ queries: map[string]string{
+ "access": "user",
+ },
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{},
+ },
+ err: s3err.GetAPIError(s3err.ErrAdminInvalidUserRole),
},
- wantErr: false,
- statusCode: 400,
},
{
- name: "Admin-update-user-not-found",
- app: appNotFound,
- args: args{
- req: httptest.NewRequest(http.MethodPatch, "/update-user?access=access", strings.NewReader(succUser)),
+ name: "backend returns user not found error",
+ input: testInput{
+ body: validBody,
+ beErr: auth.ErrNoSuchUser,
+ queries: map[string]string{
+ "access": "user",
+ },
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{},
+ },
+ err: s3err.GetAPIError(s3err.ErrAdminUserNotFound),
+ },
+ },
+ {
+ name: "backend returns other error",
+ input: testInput{
+ body: validBody,
+ beErr: s3err.GetAPIError(s3err.ErrInvalidRequest),
+ queries: map[string]string{
+ "access": "user",
+ },
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{},
+ },
+ err: s3err.GetAPIError(s3err.ErrInvalidRequest),
+ },
+ },
+ {
+ name: "successful response",
+ input: testInput{
+ body: validBody,
+ queries: map[string]string{
+ "access": "user",
+ },
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{},
+ },
},
- wantErr: false,
- statusCode: 404,
},
}
for _, tt := range tests {
- resp, err := tt.app.Test(tt.args.req)
+ t.Run(tt.name, func(t *testing.T) {
+ iam := &IAMServiceMock{
+ UpdateUserAccountFunc: func(access string, props auth.MutableProps) error {
+ return tt.input.beErr
+ },
+ }
- if (err != nil) != tt.wantErr {
- t.Errorf("AdminController.UpdateUser() error = %v, wantErr %v", err, tt.wantErr)
- }
+ ctrl := AdminController{
+ iam: iam,
+ }
- if resp.StatusCode != tt.statusCode {
- t.Errorf("AdminController.UpdateUser() statusCode = %v, wantStatusCode = %v", resp.StatusCode, tt.statusCode)
- }
+ testController(
+ t,
+ ctrl.UpdateUser,
+ tt.output.response,
+ tt.output.err,
+ ctxInputs{
+ body: tt.input.body,
+ queries: tt.input.queries,
+ })
+ })
}
}
func TestAdminController_DeleteUser(t *testing.T) {
- type args struct {
- req *http.Request
- }
-
- adminController := AdminController{
- iam: &IAMServiceMock{
- DeleteUserAccountFunc: func(access string) error {
- return nil
- },
- },
- }
-
- app := fiber.New()
-
- app.Patch("/delete-user", adminController.DeleteUser)
-
tests := []struct {
- name string
- app *fiber.App
- args args
- wantErr bool
- statusCode int
+ name string
+ input testInput
+ output testOutput
}{
{
- name: "Admin-delete-user-success",
- app: app,
- args: args{
- req: httptest.NewRequest(http.MethodPatch, "/delete-user?access=test", nil),
+ name: "missing user access key",
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{},
+ },
+ err: s3err.GetAPIError(s3err.ErrAdminMissingUserAcess),
+ },
+ },
+ {
+ name: "backend returns other error",
+ input: testInput{
+ beErr: s3err.GetAPIError(s3err.ErrInvalidRequest),
+ queries: map[string]string{
+ "access": "user",
+ },
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{},
+ },
+ err: s3err.GetAPIError(s3err.ErrInvalidRequest),
+ },
+ },
+ {
+ name: "successful response",
+ input: testInput{
+ queries: map[string]string{
+ "access": "user",
+ },
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{},
+ },
},
- wantErr: false,
- statusCode: 200,
},
}
for _, tt := range tests {
- resp, err := tt.app.Test(tt.args.req)
+ t.Run(tt.name, func(t *testing.T) {
+ iam := &IAMServiceMock{
+ DeleteUserAccountFunc: func(access string) error {
+ return tt.input.beErr
+ },
+ }
- if (err != nil) != tt.wantErr {
- t.Errorf("AdminController.DeleteUser() error = %v, wantErr %v", err, tt.wantErr)
- }
+ ctrl := AdminController{
+ iam: iam,
+ }
- if resp.StatusCode != tt.statusCode {
- t.Errorf("AdminController.DeleteUser() statusCode = %v, wantStatusCode = %v", resp.StatusCode, tt.statusCode)
- }
+ testController(
+ t,
+ ctrl.DeleteUser,
+ tt.output.response,
+ tt.output.err,
+ ctxInputs{
+ queries: tt.input.queries,
+ })
+ })
}
}
func TestAdminController_ListUsers(t *testing.T) {
- type args struct {
- req *http.Request
- }
-
- adminController := AdminController{
- iam: &IAMServiceMock{
- ListUserAccountsFunc: func() ([]auth.Account, error) {
- return []auth.Account{}, nil
- },
+ accs := []auth.Account{
+ {
+ Access: "access",
+ Secret: "secret",
+ },
+ {
+ Access: "access",
+ Secret: "secret",
},
}
-
- adminControllerErr := AdminController{
- iam: &IAMServiceMock{
- ListUserAccountsFunc: func() ([]auth.Account, error) {
- return []auth.Account{}, fmt.Errorf("server error")
- },
- },
- }
-
- appErr := fiber.New()
- appErr.Patch("/list-users", adminControllerErr.ListUsers)
-
- appSucc := fiber.New()
- appSucc.Patch("/list-users", adminController.ListUsers)
-
tests := []struct {
- name string
- app *fiber.App
- args args
- wantErr bool
- statusCode int
+ name string
+ input testInput
+ output testOutput
}{
{
- name: "Admin-list-users-iam-error",
- app: appErr,
- args: args{
- req: httptest.NewRequest(http.MethodPatch, "/list-users", nil),
+ name: "backend returns error",
+ input: testInput{
+ beRes: []auth.Account{},
+ beErr: s3err.GetAPIError(s3err.ErrInternalError),
+ },
+ output: testOutput{
+ response: &Response{
+ Data: auth.ListUserAccountsResult{
+ Accounts: []auth.Account{},
+ },
+ MetaOpts: &MetaOptions{},
+ },
+ err: s3err.GetAPIError(s3err.ErrInternalError),
},
- wantErr: false,
- statusCode: 500,
},
{
- name: "Admin-list-users-success",
- app: appSucc,
- args: args{
- req: httptest.NewRequest(http.MethodPatch, "/list-users", nil),
+ name: "successful response",
+ input: testInput{
+ beRes: accs,
+ },
+ output: testOutput{
+ response: &Response{
+ Data: auth.ListUserAccountsResult{
+ Accounts: accs,
+ },
+ MetaOpts: &MetaOptions{},
+ },
},
- wantErr: false,
- statusCode: 200,
},
}
for _, tt := range tests {
- resp, err := tt.app.Test(tt.args.req)
+ t.Run(tt.name, func(t *testing.T) {
+ iam := &IAMServiceMock{
+ ListUserAccountsFunc: func() ([]auth.Account, error) {
+ return tt.input.beRes.([]auth.Account), tt.input.beErr
+ },
+ }
- if (err != nil) != tt.wantErr {
- t.Errorf("AdminController.ListUsers() error = %v, wantErr %v", err, tt.wantErr)
- }
+ ctrl := AdminController{
+ iam: iam,
+ }
- if resp.StatusCode != tt.statusCode {
- t.Errorf("AdminController.ListUsers() statusCode = %v, wantStatusCode = %v", resp.StatusCode, tt.statusCode)
- }
+ testController(
+ t,
+ ctrl.ListUsers,
+ tt.output.response,
+ tt.output.err,
+ ctxInputs{
+ queries: tt.input.queries,
+ })
+ })
}
}
func TestAdminController_ChangeBucketOwner(t *testing.T) {
- type args struct {
- req *http.Request
- }
- adminController := AdminController{
- be: &BackendMock{
- ChangeBucketOwnerFunc: func(contextMoqParam context.Context, bucket, owner string) error {
- return nil
- },
- },
- iam: &IAMServiceMock{
- GetUserAccountFunc: func(access string) (auth.Account, error) {
- return auth.Account{}, nil
- },
- },
- }
-
- adminControllerIamErr := AdminController{
- iam: &IAMServiceMock{
- GetUserAccountFunc: func(access string) (auth.Account, error) {
- return auth.Account{}, fmt.Errorf("unknown server error")
- },
- },
- }
-
- adminControllerIamAccDoesNotExist := AdminController{
- iam: &IAMServiceMock{
- GetUserAccountFunc: func(access string) (auth.Account, error) {
- return auth.Account{}, auth.ErrNoSuchUser
- },
- },
- }
-
- app := fiber.New()
- app.Patch("/change-bucket-owner", adminController.ChangeBucketOwner)
-
- appIamErr := fiber.New()
- appIamErr.Patch("/change-bucket-owner", adminControllerIamErr.ChangeBucketOwner)
-
- appIamNoSuchUser := fiber.New()
- appIamNoSuchUser.Patch("/change-bucket-owner", adminControllerIamAccDoesNotExist.ChangeBucketOwner)
-
tests := []struct {
- name string
- app *fiber.App
- args args
- wantErr bool
- statusCode int
+ name string
+ input testInput
+ output testOutput
}{
{
- name: "Change-bucket-owner-check-account-server-error",
- app: appIamErr,
- args: args{
- req: httptest.NewRequest(http.MethodPatch, "/change-bucket-owner", nil),
+ name: "fails to get user account",
+ input: testInput{
+ extraMockErr: s3err.GetAPIError(s3err.ErrInternalError),
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{},
+ },
+ err: errors.New("check user account: "),
},
- wantErr: false,
- statusCode: 500,
},
{
- name: "Change-bucket-owner-acc-does-not-exist",
- app: appIamNoSuchUser,
- args: args{
- req: httptest.NewRequest(http.MethodPatch, "/change-bucket-owner", nil),
+ name: "user not found",
+ input: testInput{
+ extraMockErr: auth.ErrNoSuchUser,
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{},
+ },
+ err: s3err.GetAPIError(s3err.ErrAdminUserNotFound),
},
- wantErr: false,
- statusCode: 404,
},
{
- name: "Change-bucket-owner-success",
- app: app,
- args: args{
- req: httptest.NewRequest(http.MethodPatch, "/change-bucket-owner?bucket=bucket&owner=owner", nil),
+ name: "backend returns error",
+ input: testInput{
+ beErr: s3err.GetAPIError(s3err.ErrAdminMethodNotSupported),
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{},
+ },
+ err: s3err.GetAPIError(s3err.ErrAdminMethodNotSupported),
+ },
+ },
+ {
+ name: "successful response",
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{},
+ },
},
- wantErr: false,
- statusCode: 200,
},
}
for _, tt := range tests {
- resp, err := tt.app.Test(tt.args.req)
+ t.Run(tt.name, func(t *testing.T) {
+ iam := &IAMServiceMock{
+ GetUserAccountFunc: func(access string) (auth.Account, error) {
+ return auth.Account{}, tt.input.extraMockErr
+ },
+ }
+ be := &BackendMock{
+ ChangeBucketOwnerFunc: func(contextMoqParam context.Context, bucket, owner string) error {
+ return tt.input.beErr
+ },
+ }
- if (err != nil) != tt.wantErr {
- t.Errorf("AdminController.ChangeBucketOwner() error = %v, wantErr %v", err, tt.wantErr)
- }
+ ctrl := AdminController{
+ iam: iam,
+ be: be,
+ }
- if resp.StatusCode != tt.statusCode {
- t.Errorf("AdminController.ChangeBucketOwner() statusCode = %v, wantStatusCode = %v", resp.StatusCode, tt.statusCode)
- }
+ testController(
+ t,
+ ctrl.ChangeBucketOwner,
+ tt.output.response,
+ tt.output.err,
+ ctxInputs{},
+ )
+ })
}
}
func TestAdminController_ListBuckets(t *testing.T) {
- type args struct {
- req *http.Request
- }
- adminController := AdminController{
- be: &BackendMock{
- ListBucketsAndOwnersFunc: func(contextMoqParam context.Context) ([]s3response.Bucket, error) {
- return []s3response.Bucket{}, nil
- },
+ res := []s3response.Bucket{
+ {
+ Name: "bucket",
+ Owner: "owner",
},
}
- app := fiber.New()
- app.Patch("/list-buckets", adminController.ListBuckets)
-
tests := []struct {
- name string
- app *fiber.App
- args args
- wantErr bool
- statusCode int
+ name string
+ input testInput
+ output testOutput
}{
{
- name: "List-buckets-success",
- app: app,
- args: args{
- req: httptest.NewRequest(http.MethodPatch, "/list-buckets", nil),
+ name: "backend returns other error",
+ input: testInput{
+ beRes: []s3response.Bucket{},
+ beErr: s3err.GetAPIError(s3err.ErrNoSuchBucket),
+ },
+ output: testOutput{
+ response: &Response{
+ Data: s3response.ListBucketsResult{
+ Buckets: []s3response.Bucket{},
+ },
+ MetaOpts: &MetaOptions{},
+ },
+ err: s3err.GetAPIError(s3err.ErrNoSuchBucket),
+ },
+ },
+ {
+ name: "successful response",
+ input: testInput{
+ beRes: res,
+ },
+ output: testOutput{
+ response: &Response{
+ Data: s3response.ListBucketsResult{
+ Buckets: res,
+ },
+ MetaOpts: &MetaOptions{},
+ },
},
- wantErr: false,
- statusCode: 200,
},
}
for _, tt := range tests {
- resp, err := tt.app.Test(tt.args.req)
+ t.Run(tt.name, func(t *testing.T) {
+ be := &BackendMock{
+ ListBucketsAndOwnersFunc: func(contextMoqParam context.Context) ([]s3response.Bucket, error) {
+ return tt.input.beRes.([]s3response.Bucket), tt.input.beErr
+ },
+ }
- if (err != nil) != tt.wantErr {
- t.Errorf("AdminController.ListBuckets() error = %v, wantErr %v", err, tt.wantErr)
- }
+ ctrl := AdminController{
+ be: be,
+ }
- if resp.StatusCode != tt.statusCode {
- t.Errorf("AdminController.ListBuckets() statusCode = %v, wantStatusCode = %v", resp.StatusCode, tt.statusCode)
- }
+ testController(
+ t,
+ ctrl.ListBuckets,
+ tt.output.response,
+ tt.output.err,
+ ctxInputs{},
+ )
+ })
}
}
diff --git a/s3api/controllers/base.go b/s3api/controllers/base.go
index 2b8bb9c5..eff9cb3d 100644
--- a/s3api/controllers/base.go
+++ b/s3api/controllers/base.go
@@ -15,21 +15,11 @@
package controllers
import (
- "bytes"
"encoding/xml"
- "errors"
"fmt"
- "io"
- "math"
"net/http"
- "net/url"
"os"
- "strconv"
- "strings"
- "time"
- "github.com/aws/aws-sdk-go-v2/service/s3"
- "github.com/aws/aws-sdk-go-v2/service/s3/types"
"github.com/gofiber/fiber/v2"
"github.com/versity/versitygw/auth"
"github.com/versity/versitygw/backend"
@@ -39,7 +29,6 @@ import (
"github.com/versity/versitygw/s3err"
"github.com/versity/versitygw/s3event"
"github.com/versity/versitygw/s3log"
- "github.com/versity/versitygw/s3response"
)
type S3ApiController struct {
@@ -47,18 +36,28 @@ type S3ApiController struct {
iam auth.IAMService
logger s3log.AuditLogger
evSender s3event.S3EventSender
- mm *metrics.Manager
+ mm metrics.Manager
debug bool
readonly bool
}
const (
+ // time constants
iso8601Format = "20060102T150405Z"
iso8601TimeFormatExtended = "Mon Jan _2 15:04:05 2006"
- defaultContentType = "binary/octet-stream"
+ timefmt = "Mon, 02 Jan 2006 15:04:05 GMT"
+
+ maxXMLBodyLen = 4 * 1024 * 1024
+ minPartNumber = 1
+ maxPartNumber = 10000
+ defaultMaxBuckets = int32(10000)
)
-func New(be backend.Backend, iam auth.IAMService, logger s3log.AuditLogger, evs s3event.S3EventSender, mm *metrics.Manager, debug bool, readonly bool) S3ApiController {
+var (
+ xmlhdr = []byte(`` + "\n")
+)
+
+func New(be backend.Backend, iam auth.IAMService, logger s3log.AuditLogger, evs s3event.S3EventSender, mm metrics.Manager, debug bool, readonly bool) S3ApiController {
if debug {
debuglogger.SetDebugEnabled()
}
@@ -74,3826 +73,16 @@ func New(be backend.Backend, iam auth.IAMService, logger s3log.AuditLogger, evs
}
}
-func (c S3ApiController) ListBuckets(ctx *fiber.Ctx) error {
- cToken := ctx.Query("continuation-token")
- prefix := ctx.Query("prefix")
- maxBucketsStr := ctx.Query("max-buckets")
- acct := utils.ContextKeyAccount.Get(ctx).(auth.Account)
-
- var maxBuckets int32 = 10000
- if maxBucketsStr != "" {
- maxBucketsParsed, err := strconv.ParseInt(maxBucketsStr, 10, 32)
- if err != nil || maxBucketsParsed < 0 || maxBucketsParsed > 10000 {
- if c.debug {
- debuglogger.Logf("error parsing max-buckets %q: %v", maxBucketsStr, err)
- }
- return SendXMLResponse(ctx, nil, s3err.GetAPIError(s3err.ErrInvalidMaxBuckets),
- &MetaOpts{
- Logger: c.logger,
- MetricsMng: c.mm,
- Action: metrics.ActionListAllMyBuckets,
- })
- }
- maxBuckets = int32(maxBucketsParsed)
+// Returns MethodNotAllowed for unmatched routes
+func (c S3ApiController) HandleErrorRoute(err error) Controller {
+ return func(ctx *fiber.Ctx) (*Response, error) {
+ return &Response{}, err
}
-
- res, err := c.be.ListBuckets(ctx.Context(),
- s3response.ListBucketsInput{
- Owner: acct.Access,
- IsAdmin: acct.Role == auth.RoleAdmin,
- MaxBuckets: int32(maxBuckets),
- ContinuationToken: cToken,
- Prefix: prefix,
- })
- return SendXMLResponse(ctx, res, err,
- &MetaOpts{
- Logger: c.logger,
- MetricsMng: c.mm,
- Action: metrics.ActionListAllMyBuckets,
- })
}
-func (c S3ApiController) GetActions(ctx *fiber.Ctx) error {
- bucket := ctx.Params("bucket")
- key := ctx.Params("key")
- keyEnd := ctx.Params("*1")
- uploadId := ctx.Query("uploadId")
- partNumberMarker := ctx.Query("part-number-marker")
- acceptRange := ctx.Get("Range")
- 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)
- versionId := ctx.Query("versionId")
- if keyEnd != "" {
- key = strings.Join([]string{key, keyEnd}, "/")
- }
- path := ctx.Path()
- if path[len(path)-1:] == "/" && key[len(key)-1:] != "/" {
- key = key + "/"
- }
-
- if ctx.Request().URI().QueryArgs().Has("tagging") {
- 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,
- })
- if err != nil {
- return SendXMLResponse(ctx, nil, err,
- &MetaOpts{
- Logger: c.logger,
- MetricsMng: c.mm,
- Action: metrics.ActionGetObjectTagging,
- BucketOwner: parsedAcl.Owner,
- })
- }
-
- tags, err := c.be.GetObjectTagging(ctx.Context(), bucket, key)
- if err != nil {
- return SendXMLResponse(ctx, nil, err,
- &MetaOpts{
- Logger: c.logger,
- MetricsMng: c.mm,
- Action: metrics.ActionGetObjectTagging,
- BucketOwner: parsedAcl.Owner,
- })
- }
- res := s3response.Tagging{
- TagSet: s3response.TagSet{Tags: []s3response.Tag{}},
- }
-
- for key, val := range tags {
- res.TagSet.Tags = append(res.TagSet.Tags,
- s3response.Tag{Key: key, Value: val})
- }
-
- return SendXMLResponse(ctx, res, nil,
- &MetaOpts{
- Logger: c.logger,
- MetricsMng: c.mm,
- Action: metrics.ActionGetObjectTagging,
- BucketOwner: parsedAcl.Owner,
- })
- }
-
- if ctx.Request().URI().QueryArgs().Has("retention") {
- 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,
- })
- if err != nil {
- return SendXMLResponse(ctx, nil, err,
- &MetaOpts{
- Logger: c.logger,
- MetricsMng: c.mm,
- Action: metrics.ActionGetObjectRetention,
- BucketOwner: parsedAcl.Owner,
- })
- }
-
- data, err := c.be.GetObjectRetention(ctx.Context(), bucket, key, versionId)
- if err != nil {
- return SendXMLResponse(ctx, data, err,
- &MetaOpts{
- Logger: c.logger,
- MetricsMng: c.mm,
- Action: metrics.ActionGetObjectRetention,
- BucketOwner: parsedAcl.Owner,
- })
- }
-
- retention, err := auth.ParseObjectLockRetentionOutput(data)
- return SendXMLResponse(ctx, retention, err,
- &MetaOpts{
- Logger: c.logger,
- MetricsMng: c.mm,
- Action: metrics.ActionGetObjectRetention,
- BucketOwner: parsedAcl.Owner,
- })
- }
-
- if ctx.Request().URI().QueryArgs().Has("legal-hold") {
- 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,
- })
- if err != nil {
- return SendXMLResponse(ctx, nil, err,
- &MetaOpts{
- Logger: c.logger,
- MetricsMng: c.mm,
- Action: metrics.ActionGetObjectLegalHold,
- BucketOwner: parsedAcl.Owner,
- })
- }
-
- data, err := c.be.GetObjectLegalHold(ctx.Context(), bucket, key, versionId)
- return SendXMLResponse(ctx, auth.ParseObjectLegalHoldOutput(data), err,
- &MetaOpts{
- Logger: c.logger,
- MetricsMng: c.mm,
- Action: metrics.ActionGetObjectLegalHold,
- BucketOwner: parsedAcl.Owner,
- })
- }
-
- if uploadId != "" {
- if partNumberMarker != "" {
- n, err := strconv.Atoi(partNumberMarker)
- if err != nil || n < 0 {
- if err != nil && c.debug {
- debuglogger.Logf("error parsing part number marker %q: %v",
- partNumberMarker, err)
- }
- return SendResponse(ctx,
- s3err.GetAPIError(s3err.ErrInvalidPartNumberMarker),
- &MetaOpts{
- Logger: c.logger,
- MetricsMng: c.mm,
- Action: metrics.ActionListParts,
- BucketOwner: parsedAcl.Owner,
- })
- }
- }
- mxParts := ctx.Query("max-parts")
- maxParts, err := utils.ParseUint(mxParts)
- if err != nil {
- if c.debug {
- debuglogger.Logf("error parsing max parts %q: %v",
- mxParts, err)
- }
- return SendResponse(ctx,
- s3err.GetAPIError(s3err.ErrInvalidMaxParts),
- &MetaOpts{
- Logger: c.logger,
- MetricsMng: c.mm,
- Action: metrics.ActionListParts,
- BucketOwner: parsedAcl.Owner,
- })
- }
-
- 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,
- })
- if err != nil {
- return SendXMLResponse(ctx, nil, err,
- &MetaOpts{
- Logger: c.logger,
- MetricsMng: c.mm,
- Action: metrics.ActionListParts,
- BucketOwner: parsedAcl.Owner,
- })
- }
-
- res, err := c.be.ListParts(ctx.Context(), &s3.ListPartsInput{
- Bucket: &bucket,
- Key: &key,
- UploadId: &uploadId,
- PartNumberMarker: &partNumberMarker,
- MaxParts: &maxParts,
- })
- return SendXMLResponse(ctx, res, err,
- &MetaOpts{
- Logger: c.logger,
- MetricsMng: c.mm,
- Action: metrics.ActionListParts,
- BucketOwner: parsedAcl.Owner,
- })
- }
-
- if ctx.Request().URI().QueryArgs().Has("acl") {
- 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,
- })
- if err != nil {
- return SendXMLResponse(ctx, nil, err,
- &MetaOpts{
- Logger: c.logger,
- MetricsMng: c.mm,
- Action: metrics.ActionGetObjectAcl,
- BucketOwner: parsedAcl.Owner,
- })
- }
- res, err := c.be.GetObjectAcl(ctx.Context(), &s3.GetObjectAclInput{
- Bucket: &bucket,
- Key: &key,
- })
- return SendXMLResponse(ctx, res, err,
- &MetaOpts{
- Logger: c.logger,
- MetricsMng: c.mm,
- Action: metrics.ActionGetObjectAcl,
- BucketOwner: parsedAcl.Owner,
- })
- }
-
- if ctx.Request().URI().QueryArgs().Has("attributes") {
- 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,
- })
- if err != nil {
- return SendXMLResponse(ctx, nil, err,
- &MetaOpts{
- Logger: c.logger,
- MetricsMng: c.mm,
- Action: metrics.ActionGetObjectAttributes,
- BucketOwner: parsedAcl.Owner,
- })
- }
- maxParts := ctx.Get("X-Amz-Max-Parts")
- partNumberMarker := ctx.Get("X-Amz-Part-Number-Marker")
- maxPartsParsed, err := utils.ParseUint(maxParts)
- if err != nil {
- if c.debug {
- debuglogger.Logf("error parsing max parts %q: %v",
- maxParts, err)
- }
- return SendXMLResponse(ctx, nil, s3err.GetAPIError(s3err.ErrInvalidMaxParts),
- &MetaOpts{
- Logger: c.logger,
- MetricsMng: c.mm,
- Action: metrics.ActionGetObjectAttributes,
- BucketOwner: parsedAcl.Owner,
- })
- }
- attrs, err := utils.ParseObjectAttributes(ctx)
- if err != nil {
- return SendXMLResponse(ctx, nil, err,
- &MetaOpts{
- Logger: c.logger,
- MetricsMng: c.mm,
- Action: metrics.ActionGetObjectAttributes,
- BucketOwner: parsedAcl.Owner,
- })
- }
-
- res, err := c.be.GetObjectAttributes(ctx.Context(),
- &s3.GetObjectAttributesInput{
- Bucket: &bucket,
- Key: &key,
- PartNumberMarker: &partNumberMarker,
- MaxParts: &maxPartsParsed,
- VersionId: &versionId,
- })
- if err != nil {
- hdrs := []utils.CustomHeader{}
-
- if res.DeleteMarker != nil {
- hdrs = append(hdrs, utils.CustomHeader{
- Key: "x-amz-delete-marker",
- Value: "true",
- })
- }
- if getstring(res.VersionId) != "" {
- hdrs = append(hdrs, utils.CustomHeader{
- Key: "x-amz-version-id",
- Value: getstring(res.VersionId),
- })
- }
-
- utils.SetResponseHeaders(ctx, hdrs)
- return SendXMLResponse(ctx, nil, err,
- &MetaOpts{
- Logger: c.logger,
- MetricsMng: c.mm,
- Action: metrics.ActionGetObjectAttributes,
- BucketOwner: parsedAcl.Owner,
- })
- }
-
- hdrs := []utils.CustomHeader{}
-
- if getstring(res.VersionId) != "" {
- hdrs = append(hdrs, utils.CustomHeader{
- Key: "x-amz-version-id",
- Value: getstring(res.VersionId),
- })
- }
- if res.DeleteMarker != nil && *res.DeleteMarker {
- hdrs = append(hdrs, utils.CustomHeader{
- Key: "x-amz-delete-marker",
- Value: "true",
- })
- }
- if res.LastModified != nil {
- hdrs = append(hdrs, utils.CustomHeader{
- Key: "Last-Modified",
- Value: res.LastModified.UTC().Format(iso8601TimeFormatExtended),
- })
- }
-
- utils.SetResponseHeaders(ctx, hdrs)
-
- return SendXMLResponse(ctx, utils.FilterObjectAttributes(attrs, res), nil,
- &MetaOpts{
- Logger: c.logger,
- MetricsMng: c.mm,
- Action: metrics.ActionGetObjectAttributes,
- BucketOwner: parsedAcl.Owner,
- })
- }
-
- action := auth.GetObjectAction
- if 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,
- })
- if err != nil {
- return SendResponse(ctx, err,
- &MetaOpts{
- Logger: c.logger,
- MetricsMng: c.mm,
- Action: metrics.ActionGetObject,
- BucketOwner: parsedAcl.Owner,
- })
- }
-
- checksumMode := types.ChecksumMode(ctx.Get("x-amz-checksum-mode"))
- if checksumMode != "" && checksumMode != types.ChecksumModeEnabled {
- if c.debug {
- debuglogger.Logf("invalid x-amz-checksum-mode header value: %v", checksumMode)
- }
- return SendResponse(ctx, s3err.GetInvalidChecksumHeaderErr("x-amz-checksum-mode"),
- &MetaOpts{
- Logger: c.logger,
- MetricsMng: c.mm,
- Action: metrics.ActionGetObject,
- BucketOwner: parsedAcl.Owner,
- })
- }
-
- utils.ContextKeySkipResBodyLog.Set(ctx, true)
- res, err := c.be.GetObject(ctx.Context(), &s3.GetObjectInput{
- Bucket: &bucket,
- Key: &key,
- Range: &acceptRange,
- VersionId: &versionId,
- ChecksumMode: checksumMode,
- })
- if err != nil {
- if res != nil {
- utils.SetResponseHeaders(ctx, []utils.CustomHeader{
- {
- Key: "x-amz-delete-marker",
- Value: "true",
- },
- {
- Key: "Last-Modified",
- Value: res.LastModified.UTC().Format(timefmt),
- },
- })
- }
- return SendResponse(ctx, err,
- &MetaOpts{
- Logger: c.logger,
- MetricsMng: c.mm,
- Action: metrics.ActionHeadObject,
- BucketOwner: parsedAcl.Owner,
- })
- }
-
- contentType := getstring(res.ContentType)
- if contentType == "" {
- contentType = defaultContentType
- }
- acceptRanges := getstring(res.AcceptRanges)
- if acceptRanges == "" {
- acceptRanges = "bytes"
- }
-
- hdrs := []utils.CustomHeader{
- {
- Key: "Content-Type",
- Value: contentType,
- },
- {
- Key: "ETag",
- Value: getstring(res.ETag),
- },
- {
- Key: "accept-ranges",
- Value: acceptRanges,
- },
- }
- if getstring(res.ContentDisposition) != "" {
- hdrs = append(hdrs, utils.CustomHeader{
- Key: "Content-Disposition",
- Value: getstring(res.ContentDisposition),
- })
- }
- if getstring(res.ContentEncoding) != "" {
- hdrs = append(hdrs, utils.CustomHeader{
- Key: "Content-Encoding",
- Value: getstring(res.ContentEncoding),
- })
- }
- if getstring(res.ContentLanguage) != "" {
- hdrs = append(hdrs, utils.CustomHeader{
- Key: "Content-Language",
- Value: getstring(res.ContentLanguage),
- })
- }
- if getstring(res.CacheControl) != "" {
- hdrs = append(hdrs, utils.CustomHeader{
- Key: "Cache-Control",
- Value: getstring(res.CacheControl),
- })
- }
- if getstring(res.ExpiresString) != "" {
- hdrs = append(hdrs, utils.CustomHeader{
- Key: "Expires",
- Value: getstring(res.ExpiresString),
- })
- }
- if getstring(res.ContentRange) != "" {
- hdrs = append(hdrs, utils.CustomHeader{
- Key: "Content-Range",
- Value: getstring(res.ContentRange),
- })
- }
- if res.LastModified != nil {
- hdrs = append(hdrs, utils.CustomHeader{
- Key: "Last-Modified",
- Value: res.LastModified.UTC().Format(timefmt),
- })
- }
- if res.TagCount != nil {
- hdrs = append(hdrs, utils.CustomHeader{
- Key: "x-amz-tagging-count",
- Value: fmt.Sprint(*res.TagCount),
- })
- }
- if res.StorageClass != "" {
- hdrs = append(hdrs, utils.CustomHeader{
- Key: "x-amz-storage-class",
- Value: string(res.StorageClass),
- })
- }
- if res.ChecksumCRC32 != nil {
- hdrs = append(hdrs, utils.CustomHeader{
- Key: "x-amz-checksum-crc32",
- Value: *res.ChecksumCRC32,
- })
- }
- if res.ChecksumCRC32C != nil {
- hdrs = append(hdrs, utils.CustomHeader{
- Key: "x-amz-checksum-crc32c",
- Value: *res.ChecksumCRC32C,
- })
- }
- if res.ChecksumSHA1 != nil {
- hdrs = append(hdrs, utils.CustomHeader{
- Key: "x-amz-checksum-sha1",
- Value: *res.ChecksumSHA1,
- })
- }
- if res.ChecksumSHA256 != nil {
- hdrs = append(hdrs, utils.CustomHeader{
- Key: "x-amz-checksum-sha256",
- Value: *res.ChecksumSHA256,
- })
- }
- if res.ChecksumCRC64NVME != nil {
- hdrs = append(hdrs, utils.CustomHeader{
- Key: "x-amz-checksum-crc64nvme",
- Value: *res.ChecksumCRC64NVME,
- })
- }
- if res.ChecksumType != "" {
- hdrs = append(hdrs, utils.CustomHeader{
- Key: "x-amz-checksum-type",
- Value: string(res.ChecksumType),
- })
- }
-
- // Set x-amz-meta-... headers
- utils.SetMetaHeaders(ctx, res.Metadata)
- // Set other response headers
- utils.SetResponseHeaders(ctx, hdrs)
- // Set version id header
- if getstring(res.VersionId) != "" {
- utils.SetResponseHeaders(ctx, []utils.CustomHeader{
- {
- Key: "x-amz-version-id",
- Value: getstring(res.VersionId),
- },
- })
- }
-
- status := http.StatusOK
- if acceptRange != "" {
- status = http.StatusPartialContent
- }
-
- if res.Body != nil {
- // -1 will stream response body until EOF if content length not set
- contentLen := -1
- if res.ContentLength != nil {
- if *res.ContentLength > int64(math.MaxInt) {
- debuglogger.Logf("content length %v int overflow",
- *res.ContentLength)
- return SendResponse(ctx, s3err.GetAPIError(s3err.ErrInvalidRange),
- &MetaOpts{
- Logger: c.logger,
- MetricsMng: c.mm,
- Action: metrics.ActionGetObject,
- BucketOwner: parsedAcl.Owner,
- })
- }
- contentLen = int(*res.ContentLength)
- }
- utils.StreamResponseBody(ctx, res.Body, contentLen)
- }
-
- return SendResponse(ctx, nil,
- &MetaOpts{
- Logger: c.logger,
- MetricsMng: c.mm,
- Action: metrics.ActionGetObject,
- ContentLength: getint64(res.ContentLength),
- BucketOwner: parsedAcl.Owner,
- Status: status,
- })
-}
-
-func getstring(s *string) string {
- if s == nil {
- return ""
- }
- return *s
-}
-
-func getint64(i *int64) int64 {
- if i == nil {
- return 0
- }
- return *i
-}
-
-func (c S3ApiController) ListActions(ctx *fiber.Ctx) error {
- bucket := ctx.Params("bucket")
- prefix := ctx.Query("prefix")
- cToken := ctx.Query("continuation-token")
- sAfter := ctx.Query("start-after")
- marker := ctx.Query("marker")
- delimiter := ctx.Query("delimiter")
- maxkeysStr := ctx.Query("max-keys")
- keyMarker := ctx.Query("key-marker")
- maxUploadsStr := ctx.Query("max-uploads")
- uploadIdMarker := ctx.Query("upload-id-marker")
- versionIdMarker := ctx.Query("version-id-marker")
- acct := utils.ContextKeyAccount.Get(ctx).(auth.Account)
- isRoot := utils.ContextKeyIsRoot.Get(ctx).(bool)
- isPublicBucket := utils.ContextKeyPublicBucket.IsSet(ctx)
- parsedAcl := utils.ContextKeyParsedAcl.Get(ctx).(auth.ACL)
-
- if ctx.Request().URI().QueryArgs().Has("tagging") {
- 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,
- })
- if err != nil {
- return SendXMLResponse(ctx, nil, err,
- &MetaOpts{
- Logger: c.logger,
- MetricsMng: c.mm,
- Action: metrics.ActionGetBucketTagging,
- BucketOwner: parsedAcl.Owner,
- })
- }
-
- tags, err := c.be.GetBucketTagging(ctx.Context(), bucket)
- if err != nil {
- return SendXMLResponse(ctx, nil, err,
- &MetaOpts{
- Logger: c.logger,
- MetricsMng: c.mm,
- Action: metrics.ActionGetBucketTagging,
- BucketOwner: parsedAcl.Owner,
- })
- }
- resp := s3response.Tagging{
- TagSet: s3response.TagSet{Tags: []s3response.Tag{}},
- }
-
- for key, val := range tags {
- resp.TagSet.Tags = append(resp.TagSet.Tags,
- s3response.Tag{Key: key, Value: val})
- }
-
- return SendXMLResponse(ctx, resp, nil,
- &MetaOpts{
- Logger: c.logger,
- MetricsMng: c.mm,
- Action: metrics.ActionGetBucketTagging,
- BucketOwner: parsedAcl.Owner,
- })
- }
-
- if ctx.Request().URI().QueryArgs().Has("ownershipControls") {
- 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,
- })
- if err != nil {
- return SendXMLResponse(ctx, nil, err,
- &MetaOpts{
- Logger: c.logger,
- MetricsMng: c.mm,
- Action: metrics.ActionGetBucketOwnershipControls,
- BucketOwner: parsedAcl.Owner,
- })
- }
-
- data, err := c.be.GetBucketOwnershipControls(ctx.Context(), bucket)
- return SendXMLResponse(ctx,
- s3response.OwnershipControls{
- Rules: []types.OwnershipControlsRule{
- {
- ObjectOwnership: data,
- },
- },
- }, err,
- &MetaOpts{
- Logger: c.logger,
- MetricsMng: c.mm,
- Action: metrics.ActionGetBucketOwnershipControls,
- BucketOwner: parsedAcl.Owner,
- })
- }
-
- if ctx.Request().URI().QueryArgs().Has("versioning") {
- 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,
- })
- if err != nil {
- return SendXMLResponse(ctx, nil, err,
- &MetaOpts{
- Logger: c.logger,
- MetricsMng: c.mm,
- Action: metrics.ActionGetBucketVersioning,
- BucketOwner: parsedAcl.Owner,
- })
- }
- // Only admin users and the bucket owner are allowed to get the versioning state of a bucket.
- if err := auth.IsAdminOrOwner(acct, isRoot, parsedAcl); err != nil {
- return SendXMLResponse(ctx, nil, err,
- &MetaOpts{
- Logger: c.logger,
- MetricsMng: c.mm,
- Action: metrics.ActionGetBucketVersioning,
- BucketOwner: parsedAcl.Owner,
- })
- }
-
- data, err := c.be.GetBucketVersioning(ctx.Context(), bucket)
- return SendXMLResponse(ctx, data, err,
- &MetaOpts{
- Logger: c.logger,
- MetricsMng: c.mm,
- Action: metrics.ActionGetBucketVersioning,
- BucketOwner: parsedAcl.Owner,
- })
- }
-
- if ctx.Request().URI().QueryArgs().Has("policy") {
- 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,
- })
- if err != nil {
- return SendXMLResponse(ctx, nil, err,
- &MetaOpts{
- Logger: c.logger,
- MetricsMng: c.mm,
- Action: metrics.ActionGetBucketPolicy,
- BucketOwner: parsedAcl.Owner,
- })
- }
-
- data, err := c.be.GetBucketPolicy(ctx.Context(), bucket)
- return SendXMLResponse(ctx, data, err,
- &MetaOpts{
- Logger: c.logger,
- MetricsMng: c.mm,
- Action: metrics.ActionGetBucketPolicy,
- BucketOwner: parsedAcl.Owner,
- })
- }
-
- if ctx.Request().URI().QueryArgs().Has("cors") {
- 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,
- })
- if err != nil {
- return SendXMLResponse(ctx, nil, err,
- &MetaOpts{
- Logger: c.logger,
- MetricsMng: c.mm,
- Action: metrics.ActionGetBucketCors,
- BucketOwner: parsedAcl.Owner,
- })
- }
-
- data, err := c.be.GetBucketCors(ctx.Context(), bucket)
- return SendXMLResponse(ctx, data, err,
- &MetaOpts{
- Logger: c.logger,
- MetricsMng: c.mm,
- Action: metrics.ActionGetBucketCors,
- BucketOwner: parsedAcl.Owner,
- })
- }
-
- if ctx.Request().URI().QueryArgs().Has("versions") {
- 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,
- })
- if err != nil {
- return SendXMLResponse(ctx, nil, err,
- &MetaOpts{
- Logger: c.logger,
- MetricsMng: c.mm,
- Action: metrics.ActionListObjectVersions,
- BucketOwner: parsedAcl.Owner,
- })
- }
-
- maxkeys, err := utils.ParseUint(maxkeysStr)
- if err != nil {
- if c.debug {
- debuglogger.Logf("error parsing max keys %q: %v",
- maxkeysStr, err)
- }
- return SendXMLResponse(ctx, nil, s3err.GetAPIError(s3err.ErrInvalidMaxKeys),
- &MetaOpts{
- Logger: c.logger,
- MetricsMng: c.mm,
- Action: metrics.ActionListObjectVersions,
- BucketOwner: parsedAcl.Owner,
- })
- }
-
- data, err := c.be.ListObjectVersions(ctx.Context(),
- &s3.ListObjectVersionsInput{
- Bucket: &bucket,
- Delimiter: &delimiter,
- KeyMarker: &keyMarker,
- MaxKeys: &maxkeys,
- Prefix: &prefix,
- VersionIdMarker: &versionIdMarker,
- })
- return SendXMLResponse(ctx, data, err,
- &MetaOpts{
- Logger: c.logger,
- MetricsMng: c.mm,
- Action: metrics.ActionListObjectVersions,
- BucketOwner: parsedAcl.Owner,
- })
- }
-
- if ctx.Request().URI().QueryArgs().Has("object-lock") {
- 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,
- })
- if err != nil {
- return SendXMLResponse(ctx, nil, err,
- &MetaOpts{
- Logger: c.logger,
- MetricsMng: c.mm,
- Action: metrics.ActionGetObjectLockConfiguration,
- BucketOwner: parsedAcl.Owner,
- })
- }
-
- data, err := c.be.GetObjectLockConfiguration(ctx.Context(), bucket)
- if err != nil {
- return SendXMLResponse(ctx, nil, err,
- &MetaOpts{
- Logger: c.logger,
- MetricsMng: c.mm,
- Action: metrics.ActionGetObjectLockConfiguration,
- BucketOwner: parsedAcl.Owner,
- })
- }
-
- resp, err := auth.ParseBucketLockConfigurationOutput(data)
- return SendXMLResponse(ctx, resp, err,
- &MetaOpts{
- Logger: c.logger,
- MetricsMng: c.mm,
- Action: metrics.ActionGetObjectLockConfiguration,
- BucketOwner: parsedAcl.Owner,
- })
- }
-
- if ctx.Request().URI().QueryArgs().Has("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,
- })
- if err != nil {
- return SendXMLResponse(ctx, nil, err,
- &MetaOpts{
- Logger: c.logger,
- MetricsMng: c.mm,
- Action: metrics.ActionGetBucketAcl,
- BucketOwner: parsedAcl.Owner,
- })
- }
-
- data, err := c.be.GetBucketAcl(ctx.Context(),
- &s3.GetBucketAclInput{Bucket: &bucket})
- if err != nil {
- return SendResponse(ctx, err,
- &MetaOpts{
- Logger: c.logger,
- MetricsMng: c.mm,
- })
- }
-
- res, err := auth.ParseACLOutput(data, parsedAcl.Owner)
- return SendXMLResponse(ctx, res, err,
- &MetaOpts{
- Logger: c.logger,
- MetricsMng: c.mm,
- Action: metrics.ActionGetBucketAcl,
- BucketOwner: parsedAcl.Owner,
- })
- }
-
- if ctx.Request().URI().QueryArgs().Has("uploads") {
- 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,
- })
- if err != nil {
- return SendXMLResponse(ctx, nil, err,
- &MetaOpts{
- Logger: c.logger,
- MetricsMng: c.mm,
- Action: metrics.ActionListMultipartUploads,
- BucketOwner: parsedAcl.Owner,
- })
- }
- maxUploads, err := utils.ParseUint(maxUploadsStr)
- if err != nil {
- if c.debug {
- debuglogger.Logf("error parsing max uploads %q: %v",
- maxUploadsStr, err)
- }
- return SendXMLResponse(ctx, nil, s3err.GetAPIError(s3err.ErrInvalidMaxUploads),
- &MetaOpts{
- Logger: c.logger,
- MetricsMng: c.mm,
- Action: metrics.ActionListMultipartUploads,
- BucketOwner: parsedAcl.Owner,
- })
- }
- res, err := c.be.ListMultipartUploads(ctx.Context(),
- &s3.ListMultipartUploadsInput{
- Bucket: &bucket,
- Delimiter: &delimiter,
- Prefix: &prefix,
- UploadIdMarker: &uploadIdMarker,
- MaxUploads: &maxUploads,
- KeyMarker: &keyMarker,
- })
- return SendXMLResponse(ctx, res, err,
- &MetaOpts{
- Logger: c.logger,
- MetricsMng: c.mm,
- Action: metrics.ActionListMultipartUploads,
- BucketOwner: parsedAcl.Owner,
- })
- }
-
- if ctx.QueryInt("list-type") == 2 {
- 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,
- })
- if err != nil {
- return SendXMLResponse(ctx, nil, err,
- &MetaOpts{
- Logger: c.logger,
- MetricsMng: c.mm,
- Action: metrics.ActionListObjectsV2,
- BucketOwner: parsedAcl.Owner,
- })
- }
- maxkeys, err := utils.ParseUint(maxkeysStr)
- if err != nil {
- if c.debug {
- debuglogger.Logf("error parsing max keys %q: %v",
- maxkeysStr, err)
- }
- return SendXMLResponse(ctx, nil, s3err.GetAPIError(s3err.ErrInvalidMaxKeys),
- &MetaOpts{
- Logger: c.logger,
- MetricsMng: c.mm,
- Action: metrics.ActionListObjectsV2,
- BucketOwner: parsedAcl.Owner,
- })
- }
-
- fetchOwner := strings.EqualFold(ctx.Query("fetch-owner"), "true")
- res, err := c.be.ListObjectsV2(ctx.Context(),
- &s3.ListObjectsV2Input{
- Bucket: &bucket,
- Prefix: &prefix,
- ContinuationToken: &cToken,
- Delimiter: &delimiter,
- MaxKeys: &maxkeys,
- StartAfter: &sAfter,
- FetchOwner: &fetchOwner,
- })
- return SendXMLResponse(ctx, res, err,
- &MetaOpts{
- Logger: c.logger,
- MetricsMng: c.mm,
- Action: metrics.ActionListObjectsV2,
- BucketOwner: parsedAcl.Owner,
- })
- }
-
- 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,
- })
- if err != nil {
- return SendXMLResponse(ctx, nil, err,
- &MetaOpts{
- Logger: c.logger,
- MetricsMng: c.mm,
- Action: metrics.ActionListObjects,
- BucketOwner: parsedAcl.Owner,
- })
- }
-
- maxkeys, err := utils.ParseUint(maxkeysStr)
- if err != nil {
- if c.debug {
- debuglogger.Logf("error parsing max keys %q: %v",
- maxkeysStr, err)
- }
- return SendXMLResponse(ctx, nil, s3err.GetAPIError(s3err.ErrInvalidMaxKeys),
- &MetaOpts{
- Logger: c.logger,
- MetricsMng: c.mm,
- Action: metrics.ActionListObjects,
- BucketOwner: parsedAcl.Owner,
- })
- }
-
- res, err := c.be.ListObjects(ctx.Context(),
- &s3.ListObjectsInput{
- Bucket: &bucket,
- Prefix: &prefix,
- Marker: &marker,
- Delimiter: &delimiter,
- MaxKeys: &maxkeys,
- })
- return SendXMLResponse(ctx, res, err,
- &MetaOpts{
- Logger: c.logger,
- MetricsMng: c.mm,
- Action: metrics.ActionListObjects,
- BucketOwner: parsedAcl.Owner,
- })
-}
-
-func (c S3ApiController) PutBucketActions(ctx *fiber.Ctx) error {
- bucket := ctx.Params("bucket")
- acl := ctx.Get("X-Amz-Acl")
- grantFullControl := ctx.Get("X-Amz-Grant-Full-Control")
- grantRead := ctx.Get("X-Amz-Grant-Read")
- grantReadACP := ctx.Get("X-Amz-Grant-Read-Acp")
- granWrite := ctx.Get("X-Amz-Grant-Write")
- grantWriteACP := ctx.Get("X-Amz-Grant-Write-Acp")
- objectOwnership := types.ObjectOwnership(
- ctx.Get("X-Amz-Object-Ownership", string(types.ObjectOwnershipBucketOwnerEnforced)),
- )
- // mfa := ctx.Get("X-Amz-Mfa")
- // contentMD5 := ctx.Get("Content-MD5")
- acct := utils.ContextKeyAccount.Get(ctx).(auth.Account)
- isRoot := utils.ContextKeyIsRoot.Get(ctx).(bool)
- isPublicBucket := utils.ContextKeyPublicBucket.IsSet(ctx)
-
- if ctx.Request().URI().QueryArgs().Has("tagging") {
- parsedAcl := utils.ContextKeyParsedAcl.Get(ctx).(auth.ACL)
-
- tagging, err := utils.ParseTagging(ctx.Body(), utils.TagLimitBucket)
- if err != nil {
- return SendResponse(ctx, err,
- &MetaOpts{
- Logger: c.logger,
- MetricsMng: c.mm,
- Action: metrics.ActionPutBucketTagging,
- BucketOwner: parsedAcl.Owner,
- })
- }
-
- 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,
- })
- if err != nil {
- return SendResponse(ctx, err,
- &MetaOpts{
- Logger: c.logger,
- MetricsMng: c.mm,
- Action: metrics.ActionPutBucketTagging,
- BucketOwner: parsedAcl.Owner,
- })
- }
-
- err = c.be.PutBucketTagging(ctx.Context(), bucket, tagging)
- return SendResponse(ctx, err,
- &MetaOpts{
- Logger: c.logger,
- MetricsMng: c.mm,
- Action: metrics.ActionPutBucketTagging,
- BucketOwner: parsedAcl.Owner,
- Status: http.StatusNoContent,
- })
- }
-
- if ctx.Request().URI().QueryArgs().Has("ownershipControls") {
- parsedAcl := utils.ContextKeyParsedAcl.Get(ctx).(auth.ACL)
- var ownershipControls s3response.OwnershipControls
- if err := xml.Unmarshal(ctx.Body(), &ownershipControls); err != nil {
- if c.debug {
- debuglogger.Logf("failed to unmarshal request body: %v", err)
- }
- return SendResponse(ctx, s3err.GetAPIError(s3err.ErrMalformedXML),
- &MetaOpts{
- Logger: c.logger,
- MetricsMng: c.mm,
- Action: metrics.ActionPutBucketOwnershipControls,
- BucketOwner: parsedAcl.Owner,
- })
- }
-
- rulesCount := len(ownershipControls.Rules)
- isValidOwnership := utils.IsValidOwnership(ownershipControls.Rules[0].ObjectOwnership)
- if rulesCount != 1 || !isValidOwnership {
- if c.debug && rulesCount != 1 {
- debuglogger.Logf("ownership control rules should be 1, got %v", rulesCount)
- }
- return SendResponse(ctx, s3err.GetAPIError(s3err.ErrMalformedXML),
- &MetaOpts{
- Logger: c.logger,
- MetricsMng: c.mm,
- Action: metrics.ActionPutBucketOwnershipControls,
- BucketOwner: parsedAcl.Owner,
- })
- }
-
- 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.PutBucketOwnershipControlsAction,
- }); err != nil {
- return SendResponse(ctx, err,
- &MetaOpts{
- Logger: c.logger,
- MetricsMng: c.mm,
- Action: metrics.ActionPutBucketOwnershipControls,
- BucketOwner: parsedAcl.Owner,
- })
- }
-
- err := c.be.PutBucketOwnershipControls(ctx.Context(), bucket, ownershipControls.Rules[0].ObjectOwnership)
- return SendResponse(ctx, err,
- &MetaOpts{
- Logger: c.logger,
- MetricsMng: c.mm,
- Action: metrics.ActionPutBucketOwnershipControls,
- BucketOwner: parsedAcl.Owner,
- })
- }
-
- if ctx.Request().URI().QueryArgs().Has("versioning") {
- 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,
- Action: auth.PutBucketVersioningAction,
- IsBucketPublic: isPublicBucket,
- })
- if err != nil {
- return SendResponse(ctx, err,
- &MetaOpts{
- Logger: c.logger,
- MetricsMng: c.mm,
- Action: metrics.ActionPutBucketVersioning,
- BucketOwner: parsedAcl.Owner,
- })
- }
-
- var versioningConf types.VersioningConfiguration
- err = xml.Unmarshal(ctx.Body(), &versioningConf)
- if err != nil {
- if c.debug {
- debuglogger.Logf("error unmarshalling versioning configuration: %v",
- err)
- }
- return SendResponse(ctx, s3err.GetAPIError(s3err.ErrInvalidRequest),
- &MetaOpts{
- Logger: c.logger,
- MetricsMng: c.mm,
- Action: metrics.ActionPutBucketVersioning,
- BucketOwner: parsedAcl.Owner,
- })
- }
-
- if versioningConf.Status != types.BucketVersioningStatusEnabled &&
- versioningConf.Status != types.BucketVersioningStatusSuspended {
- if c.debug {
- debuglogger.Logf("invalid versioning configuration status: %v", versioningConf.Status)
- }
- return SendResponse(ctx, s3err.GetAPIError(s3err.ErrMalformedXML),
- &MetaOpts{
- Logger: c.logger,
- MetricsMng: c.mm,
- Action: metrics.ActionPutBucketVersioning,
- BucketOwner: parsedAcl.Owner,
- })
- }
-
- err = c.be.PutBucketVersioning(ctx.Context(), bucket, versioningConf.Status)
- return SendResponse(ctx, err,
- &MetaOpts{
- Logger: c.logger,
- MetricsMng: c.mm,
- Action: metrics.ActionPutBucketVersioning,
- BucketOwner: parsedAcl.Owner,
- })
- }
-
- if ctx.Request().URI().QueryArgs().Has("object-lock") {
- 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,
- Action: auth.PutBucketObjectLockConfigurationAction,
- IsBucketPublic: isPublicBucket,
- }); err != nil {
- return SendResponse(ctx, err,
- &MetaOpts{
- Logger: c.logger,
- MetricsMng: c.mm,
- Action: metrics.ActionPutObjectLockConfiguration,
- BucketOwner: parsedAcl.Owner,
- })
- }
-
- config, err := auth.ParseBucketLockConfigurationInput(ctx.Body())
- if err != nil {
- return SendResponse(ctx, err,
- &MetaOpts{
- Logger: c.logger,
- MetricsMng: c.mm,
- Action: metrics.ActionPutObjectLockConfiguration,
- BucketOwner: parsedAcl.Owner,
- })
- }
-
- err = c.be.PutObjectLockConfiguration(ctx.Context(), bucket, config)
- return SendResponse(ctx, err,
- &MetaOpts{
- Logger: c.logger,
- MetricsMng: c.mm,
- Action: metrics.ActionPutObjectLockConfiguration,
- BucketOwner: parsedAcl.Owner,
- })
- }
-
- if ctx.Request().URI().QueryArgs().Has("cors") {
- 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,
- Action: auth.PutBucketCorsAction,
- IsBucketPublic: isPublicBucket,
- })
- if err != nil {
- return SendResponse(ctx, err,
- &MetaOpts{
- Logger: c.logger,
- MetricsMng: c.mm,
- Action: metrics.ActionPutBucketCors,
- BucketOwner: parsedAcl.Owner,
- })
- }
-
- err = c.be.PutBucketCors(ctx.Context(), []byte{})
- if err != nil {
- return SendResponse(ctx, err,
- &MetaOpts{
- Logger: c.logger,
- MetricsMng: c.mm,
- Action: metrics.ActionPutBucketCors,
- BucketOwner: parsedAcl.Owner,
- },
- )
- }
- }
- if ctx.Request().URI().QueryArgs().Has("policy") {
- 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,
- Action: auth.PutBucketPolicyAction,
- })
- if err != nil {
- return SendResponse(ctx, err,
- &MetaOpts{
- Logger: c.logger,
- MetricsMng: c.mm,
- Action: metrics.ActionPutBucketPolicy,
- BucketOwner: parsedAcl.Owner,
- })
- }
-
- err = auth.ValidatePolicyDocument(ctx.Body(), bucket, c.iam)
- if err != nil {
- return SendResponse(ctx, err,
- &MetaOpts{
- Logger: c.logger,
- MetricsMng: c.mm,
- Action: metrics.ActionPutBucketPolicy,
- BucketOwner: parsedAcl.Owner,
- },
- )
- }
-
- err = c.be.PutBucketPolicy(ctx.Context(), bucket, ctx.Body())
- return SendResponse(ctx, err,
- &MetaOpts{
- Logger: c.logger,
- MetricsMng: c.mm,
- Action: metrics.ActionPutBucketPolicy,
- BucketOwner: parsedAcl.Owner,
- })
- }
-
- grants := grantFullControl + grantRead + grantReadACP + granWrite + grantWriteACP
-
- if ctx.Request().URI().QueryArgs().Has("acl") {
- parsedAcl := utils.ContextKeyParsedAcl.Get(ctx).(auth.ACL)
- var input *auth.PutBucketAclInput
-
- ownership, err := c.be.GetBucketOwnershipControls(ctx.Context(), bucket)
- if err != nil && !errors.Is(err, s3err.GetAPIError(s3err.ErrOwnershipControlsNotFound)) {
- return SendResponse(ctx, err,
- &MetaOpts{
- Logger: c.logger,
- MetricsMng: c.mm,
- Action: metrics.ActionPutBucketAcl,
- BucketOwner: parsedAcl.Owner,
- })
- }
- if ownership == types.ObjectOwnershipBucketOwnerEnforced {
- if c.debug {
- debuglogger.Logf("bucket acls are disabled")
- }
- return SendResponse(ctx, s3err.GetAPIError(s3err.ErrAclNotSupported),
- &MetaOpts{
- Logger: c.logger,
- MetricsMng: c.mm,
- Action: metrics.ActionPutBucketAcl,
- BucketOwner: parsedAcl.Owner,
- })
- }
-
- err = auth.VerifyAccess(ctx.Context(), c.be,
- auth.AccessOptions{
- Readonly: c.readonly,
- Acl: parsedAcl,
- AclPermission: auth.PermissionWriteAcp,
- IsRoot: isRoot,
- Acc: acct,
- Bucket: bucket,
- Action: auth.PutBucketAclAction,
- })
- if err != nil {
- return SendResponse(ctx, err,
- &MetaOpts{
- Logger: c.logger,
- MetricsMng: c.mm,
- Action: metrics.ActionPutBucketAcl,
- BucketOwner: parsedAcl.Owner,
- })
- }
-
- if len(ctx.Body()) > 0 {
- var accessControlPolicy auth.AccessControlPolicy
- err := xml.Unmarshal(ctx.Body(), &accessControlPolicy)
- if err != nil {
- if c.debug {
- debuglogger.Logf("error unmarshalling access control policy: %v", err)
- }
- return SendResponse(ctx, s3err.GetAPIError(s3err.ErrMalformedACL),
- &MetaOpts{
- Logger: c.logger,
- Action: metrics.ActionPutBucketAcl,
- BucketOwner: parsedAcl.Owner,
- })
- }
-
- err = accessControlPolicy.Validate()
- if err != nil {
- if c.debug {
- debuglogger.Logf("invalid access control policy: %v", err)
- }
- return SendResponse(ctx, err,
- &MetaOpts{
- Logger: c.logger,
- Action: metrics.ActionPutBucketAcl,
- BucketOwner: parsedAcl.Owner,
- })
- }
-
- if *accessControlPolicy.Owner.ID != parsedAcl.Owner {
- if c.debug {
- debuglogger.Logf("invalid access control policy owner id: %v, expected %v", *accessControlPolicy.Owner.ID, parsedAcl.Owner)
- }
- return SendResponse(ctx, s3err.APIError{
- Code: "InvalidArgument",
- Description: "Invalid id",
- HTTPStatusCode: http.StatusBadRequest,
- },
- &MetaOpts{
- Logger: c.logger,
- Action: metrics.ActionPutBucketAcl,
- BucketOwner: parsedAcl.Owner,
- })
- }
-
- if grants+acl != "" {
- if c.debug {
- debuglogger.Logf("invalid request: %q (grants) %q (acl)",
- grants, acl)
- }
- return SendResponse(ctx,
- s3err.GetAPIError(s3err.ErrUnexpectedContent),
- &MetaOpts{
- Logger: c.logger,
- MetricsMng: c.mm,
- Action: metrics.ActionPutBucketAcl,
- BucketOwner: parsedAcl.Owner,
- })
- }
-
- input = &auth.PutBucketAclInput{
- Bucket: &bucket,
- AccessControlPolicy: &accessControlPolicy,
- }
- } else if acl != "" {
- if acl != "private" && acl != "public-read" && acl != "public-read-write" {
- if c.debug {
- debuglogger.Logf("invalid acl: %q", acl)
- }
- return SendResponse(ctx,
- s3err.GetAPIError(s3err.ErrInvalidRequest),
- &MetaOpts{
- Logger: c.logger,
- MetricsMng: c.mm,
- Action: metrics.ActionPutBucketAcl,
- BucketOwner: parsedAcl.Owner,
- })
- }
- if grants != "" {
- if c.debug {
- debuglogger.Logf("invalid request: %q (grants) %q (acl)",
- grants, acl)
- }
- return SendResponse(ctx,
- s3err.GetAPIError(s3err.ErrBothCannedAndHeaderGrants),
- &MetaOpts{
- Logger: c.logger,
- MetricsMng: c.mm,
- Action: metrics.ActionPutBucketAcl,
- BucketOwner: parsedAcl.Owner,
- })
- }
-
- input = &auth.PutBucketAclInput{
- Bucket: &bucket,
- ACL: types.BucketCannedACL(acl),
- }
- } else if grants != "" {
- input = &auth.PutBucketAclInput{
- Bucket: &bucket,
- GrantFullControl: &grantFullControl,
- GrantRead: &grantRead,
- GrantReadACP: &grantReadACP,
- GrantWrite: &granWrite,
- GrantWriteACP: &grantWriteACP,
- }
- } else {
- if c.debug {
- debuglogger.Logf("none of the bucket acl options has been specified: canned, req headers, req body")
- }
- return SendResponse(ctx,
- s3err.GetAPIError(s3err.ErrMissingSecurityHeader),
- &MetaOpts{
- Logger: c.logger,
- MetricsMng: c.mm,
- Action: metrics.ActionPutBucketAcl,
- BucketOwner: parsedAcl.Owner,
- })
- }
-
- updAcl, err := auth.UpdateACL(input, parsedAcl, c.iam, acct.Role == auth.RoleAdmin)
- if err != nil {
- return SendResponse(ctx, err,
- &MetaOpts{
- Logger: c.logger,
- MetricsMng: c.mm,
- Action: metrics.ActionPutBucketAcl,
- BucketOwner: parsedAcl.Owner,
- })
- }
-
- err = c.be.PutBucketAcl(ctx.Context(), bucket, updAcl)
- return SendResponse(ctx, err,
- &MetaOpts{
- Logger: c.logger,
- MetricsMng: c.mm,
- Action: metrics.ActionPutBucketAcl,
- BucketOwner: parsedAcl.Owner,
- })
- }
-
- if ok := utils.IsValidOwnership(objectOwnership); !ok {
- return SendResponse(ctx, s3err.APIError{
- Code: "InvalidArgument",
- Description: fmt.Sprintf("Invalid x-amz-object-ownership header: %v", objectOwnership),
- HTTPStatusCode: http.StatusBadRequest,
- },
- &MetaOpts{
- Logger: c.logger,
- MetricsMng: c.mm,
- Action: metrics.ActionCreateBucket,
- BucketOwner: acct.Access,
- })
- }
-
- if acl+grants != "" && objectOwnership == types.ObjectOwnershipBucketOwnerEnforced {
- if c.debug {
- debuglogger.Logf("bucket acls are disabled for %v object ownership", objectOwnership)
- }
- return SendResponse(ctx, s3err.GetAPIError(s3err.ErrInvalidBucketAclWithObjectOwnership),
- &MetaOpts{
- Logger: c.logger,
- MetricsMng: c.mm,
- Action: metrics.ActionCreateBucket,
- BucketOwner: acct.Access,
- })
- }
-
- if acl != "" && grants != "" {
- if c.debug {
- debuglogger.Logf("invalid request: %q (grants) %q (acl)", grants, acl)
- }
- return SendResponse(ctx, s3err.GetAPIError(s3err.ErrBothCannedAndHeaderGrants),
- &MetaOpts{
- Logger: c.logger,
- MetricsMng: c.mm,
- Action: metrics.ActionCreateBucket,
- BucketOwner: acct.Access,
- })
- }
-
- defACL := auth.ACL{
- Owner: acct.Access,
- }
-
- updAcl, err := auth.UpdateACL(&auth.PutBucketAclInput{
- GrantFullControl: &grantFullControl,
- GrantRead: &grantRead,
- GrantReadACP: &grantReadACP,
- GrantWrite: &granWrite,
- GrantWriteACP: &grantWriteACP,
- AccessControlPolicy: &auth.AccessControlPolicy{
- Owner: &types.Owner{
- ID: &acct.Access,
- }},
- ACL: types.BucketCannedACL(acl),
- }, defACL, c.iam, acct.Role == auth.RoleAdmin)
- if err != nil {
- if c.debug {
- debuglogger.Logf("failed to update bucket acl: %v", err)
- }
- return SendResponse(ctx, err,
- &MetaOpts{
- Logger: c.logger,
- MetricsMng: c.mm,
- Action: metrics.ActionCreateBucket,
- BucketOwner: acct.Access,
- })
- }
-
- lockHeader := ctx.Get("X-Amz-Bucket-Object-Lock-Enabled")
- // CLI provides "True", SDK - "true"
- lockEnabled := lockHeader == "True" || lockHeader == "true"
-
- err = c.be.CreateBucket(ctx.Context(), &s3.CreateBucketInput{
- Bucket: &bucket,
- ObjectOwnership: objectOwnership,
- ObjectLockEnabledForBucket: &lockEnabled,
- }, updAcl)
- return SendResponse(ctx, err,
- &MetaOpts{
- Logger: c.logger,
- MetricsMng: c.mm,
- Action: metrics.ActionCreateBucket,
- BucketOwner: acct.Access,
- })
-}
-
-func (c S3ApiController) PutActions(ctx *fiber.Ctx) error {
- bucket := ctx.Params("bucket")
- keyStart := ctx.Params("key")
- keyEnd := ctx.Params("*1")
- uploadId := ctx.Query("uploadId")
- versionId := ctx.Query("versionId")
- acct := utils.ContextKeyAccount.Get(ctx).(auth.Account)
- isRoot := utils.ContextKeyIsRoot.Get(ctx).(bool)
- IsBucketPublic := utils.ContextKeyPublicBucket.IsSet(ctx)
- contentType := ctx.Get("Content-Type")
- contentEncoding := ctx.Get("Content-Encoding")
- contentDisposition := ctx.Get("Content-Disposition")
- contentLanguage := ctx.Get("Content-Language")
- cacheControl := ctx.Get("Cache-Control")
- expires := ctx.Get("Expires")
- parsedAcl := utils.ContextKeyParsedAcl.Get(ctx).(auth.ACL)
- tagging := ctx.Get("x-amz-tagging")
-
- // Copy source headers
- copySource := ctx.Get("X-Amz-Copy-Source")
- if len(copySource) > 0 && copySource[0] == '/' {
- copySource = copySource[1:]
- }
- copySrcIfMatch := ctx.Get("X-Amz-Copy-Source-If-Match")
- copySrcIfNoneMatch := ctx.Get("X-Amz-Copy-Source-If-None-Match")
- copySrcModifSince := ctx.Get("X-Amz-Copy-Source-If-Modified-Since")
- copySrcUnmodifSince := ctx.Get("X-Amz-Copy-Source-If-Unmodified-Since")
- copySrcRange := ctx.Get("X-Amz-Copy-Source-Range")
- directive := ctx.Get("X-Amz-Metadata-Directive")
-
- // Permission headers
- acl := ctx.Get("X-Amz-Acl")
- grantFullControl := ctx.Get("X-Amz-Grant-Full-Control")
- grantRead := ctx.Get("X-Amz-Grant-Read")
- grantReadACP := ctx.Get("X-Amz-Grant-Read-Acp")
- granWrite := ctx.Get("X-Amz-Grant-Write")
- grantWriteACP := ctx.Get("X-Amz-Grant-Write-Acp")
-
- // Content Length
- contentLengthStr := ctx.Get("Content-Length")
- if contentLengthStr == "" {
- contentLengthStr = "0"
- }
- // Use decoded content length if available because the
- // middleware will decode the chunked transfer encoding
- decodedLength := ctx.Get("X-Amz-Decoded-Content-Length")
- if decodedLength != "" {
- contentLengthStr = decodedLength
- }
-
- // Other headers
- bucketOwner := ctx.Get("X-Amz-Expected-Bucket-Owner")
- storageClass := ctx.Get("X-Amz-Storage-Class")
-
- grants := grantFullControl + grantRead + grantReadACP + granWrite + grantWriteACP
-
- if keyEnd != "" {
- keyStart = strings.Join([]string{keyStart, keyEnd}, "/")
- }
- path := ctx.Path()
- if path[len(path)-1:] == "/" && keyStart[len(keyStart)-1:] != "/" {
- keyStart = keyStart + "/"
- }
-
- if ctx.Request().URI().QueryArgs().Has("tagging") {
- tagging, err := utils.ParseTagging(ctx.Body(), utils.TagLimitObject)
- if err != nil {
- return SendResponse(ctx, err,
- &MetaOpts{
- Logger: c.logger,
- MetricsMng: c.mm,
- Action: metrics.ActionPutObjectTagging,
- BucketOwner: parsedAcl.Owner,
- })
- }
-
- err = auth.VerifyAccess(ctx.Context(), c.be, auth.AccessOptions{
- Readonly: c.readonly,
- Acl: parsedAcl,
- AclPermission: auth.PermissionWrite,
- IsRoot: isRoot,
- Acc: acct,
- Bucket: bucket,
- Object: keyStart,
- Action: auth.PutBucketTaggingAction,
- IsBucketPublic: IsBucketPublic,
- })
- if err != nil {
- return SendResponse(ctx, err,
- &MetaOpts{
- Logger: c.logger,
- MetricsMng: c.mm,
- Action: metrics.ActionPutObjectTagging,
- BucketOwner: parsedAcl.Owner,
- })
- }
-
- err = c.be.PutObjectTagging(ctx.Context(), bucket, keyStart, tagging)
- return SendResponse(ctx, err,
- &MetaOpts{
- Logger: c.logger,
- MetricsMng: c.mm,
- EvSender: c.evSender,
- Action: metrics.ActionPutObjectTagging,
- BucketOwner: parsedAcl.Owner,
- EventName: s3event.EventObjectTaggingPut,
- })
- }
-
- if ctx.Request().URI().QueryArgs().Has("retention") {
- 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: keyStart,
- Action: auth.PutObjectRetentionAction,
- IsBucketPublic: IsBucketPublic,
- }); err != nil {
- return SendResponse(ctx, err,
- &MetaOpts{
- Logger: c.logger,
- MetricsMng: c.mm,
- Action: metrics.ActionPutObjectRetention,
- BucketOwner: parsedAcl.Owner,
- })
- }
-
- bypassHdr := ctx.Get("X-Amz-Bypass-Governance-Retention")
- bypass := strings.EqualFold(bypassHdr, "true")
- if bypass {
- policy, err := c.be.GetBucketPolicy(ctx.Context(), bucket)
- if err != nil {
- bypass = false
- } else {
- if err := auth.VerifyBucketPolicy(policy, acct.Access, bucket, keyStart, auth.BypassGovernanceRetentionAction); err != nil {
- bypass = false
- }
- }
- }
-
- retention, err := auth.ParseObjectLockRetentionInput(ctx.Body())
- if err != nil {
- if c.debug {
- debuglogger.Logf("failed to parse object lock configuration input: %v", err)
- }
- return SendResponse(ctx, err, &MetaOpts{
- Logger: c.logger,
- MetricsMng: c.mm,
- Action: metrics.ActionPutObjectRetention,
- BucketOwner: parsedAcl.Owner,
- })
- }
-
- err = c.be.PutObjectRetention(ctx.Context(), bucket, keyStart, versionId, bypass, retention)
- return SendResponse(ctx, err, &MetaOpts{
- Logger: c.logger,
- MetricsMng: c.mm,
- Action: metrics.ActionPutObjectRetention,
- BucketOwner: parsedAcl.Owner,
- })
- }
-
- if ctx.Request().URI().QueryArgs().Has("legal-hold") {
- var legalHold types.ObjectLockLegalHold
- if err := xml.Unmarshal(ctx.Body(), &legalHold); err != nil {
- if c.debug {
- debuglogger.Logf("failed to parse request body: %v", err)
- }
- return SendResponse(ctx, s3err.GetAPIError(s3err.ErrMalformedXML),
- &MetaOpts{
- Logger: c.logger,
- MetricsMng: c.mm,
- Action: metrics.ActionPutObjectLegalHold,
- BucketOwner: parsedAcl.Owner,
- })
- }
-
- if legalHold.Status != types.ObjectLockLegalHoldStatusOff && legalHold.Status != types.ObjectLockLegalHoldStatusOn {
- if c.debug {
- debuglogger.Logf("invalid legal hold status: %v", legalHold.Status)
- }
- return SendResponse(ctx, s3err.GetAPIError(s3err.ErrMalformedXML),
- &MetaOpts{
- Logger: c.logger,
- MetricsMng: c.mm,
- Action: metrics.ActionPutObjectLegalHold,
- BucketOwner: parsedAcl.Owner,
- })
- }
-
- 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: keyStart,
- Action: auth.PutObjectLegalHoldAction,
- IsBucketPublic: IsBucketPublic,
- }); err != nil {
- return SendResponse(ctx, err,
- &MetaOpts{
- Logger: c.logger,
- MetricsMng: c.mm,
- Action: metrics.ActionPutObjectLegalHold,
- BucketOwner: parsedAcl.Owner,
- })
- }
-
- err := c.be.PutObjectLegalHold(ctx.Context(), bucket, keyStart, versionId, legalHold.Status == types.ObjectLockLegalHoldStatusOn)
- return SendResponse(ctx, err, &MetaOpts{
- Logger: c.logger,
- MetricsMng: c.mm,
- Action: metrics.ActionPutObjectLegalHold,
- BucketOwner: parsedAcl.Owner,
- })
- }
-
- if ctx.Request().URI().QueryArgs().Has("uploadId") &&
- ctx.Request().URI().QueryArgs().Has("partNumber") &&
- copySource != "" {
-
- cs := copySource
- copySource, err := url.QueryUnescape(copySource)
- if err != nil {
- if c.debug {
- debuglogger.Logf("error unescaping copy source %q: %v",
- cs, err)
- }
- return SendXMLResponse(ctx, nil,
- s3err.GetAPIError(s3err.ErrInvalidCopySource),
- &MetaOpts{
- Logger: c.logger,
- MetricsMng: c.mm,
- Action: metrics.ActionUploadPartCopy,
- BucketOwner: parsedAcl.Owner,
- })
- }
-
- partNumber := int32(ctx.QueryInt("partNumber", -1))
- if partNumber < 1 || partNumber > 10000 {
- if c.debug {
- debuglogger.Logf("invalid part number: %d", partNumber)
- }
- return SendXMLResponse(ctx, nil,
- s3err.GetAPIError(s3err.ErrInvalidPartNumber),
- &MetaOpts{
- Logger: c.logger,
- MetricsMng: c.mm,
- Action: metrics.ActionUploadPartCopy,
- BucketOwner: parsedAcl.Owner,
- })
- }
-
- err = auth.VerifyObjectCopyAccess(ctx.Context(), c.be, copySource,
- auth.AccessOptions{
- Acl: parsedAcl,
- AclPermission: auth.PermissionWrite,
- IsRoot: isRoot,
- Acc: acct,
- Bucket: bucket,
- Object: keyStart,
- Action: auth.PutObjectAction,
- IsBucketPublic: IsBucketPublic,
- })
- if err != nil {
- return SendXMLResponse(ctx, nil, err,
- &MetaOpts{
- Logger: c.logger,
- MetricsMng: c.mm,
- Action: metrics.ActionUploadPartCopy,
- BucketOwner: parsedAcl.Owner,
- })
- }
-
- resp, err := c.be.UploadPartCopy(ctx.Context(),
- &s3.UploadPartCopyInput{
- Bucket: &bucket,
- Key: &keyStart,
- CopySource: ©Source,
- PartNumber: &partNumber,
- UploadId: &uploadId,
- ExpectedBucketOwner: &bucketOwner,
- CopySourceRange: ©SrcRange,
- })
- if err == nil && resp.CopySourceVersionId != "" {
- utils.SetResponseHeaders(ctx, []utils.CustomHeader{
- {
- Key: "x-amz-copy-source-version-id",
- Value: resp.CopySourceVersionId,
- },
- })
- }
- return SendXMLResponse(ctx, resp, err,
- &MetaOpts{
- Logger: c.logger,
- MetricsMng: c.mm,
- Action: metrics.ActionUploadPartCopy,
- BucketOwner: parsedAcl.Owner,
- })
- }
-
- if ctx.Request().URI().QueryArgs().Has("uploadId") &&
- ctx.Request().URI().QueryArgs().Has("partNumber") {
- partNumber := int32(ctx.QueryInt("partNumber", -1))
- if partNumber < 1 || partNumber > 10000 {
- if c.debug {
- debuglogger.Logf("invalid part number: %d", partNumber)
- }
- return SendResponse(ctx, s3err.GetAPIError(s3err.ErrInvalidPartNumber),
- &MetaOpts{
- Logger: c.logger,
- MetricsMng: c.mm,
- Action: metrics.ActionUploadPart,
- BucketOwner: parsedAcl.Owner,
- })
- }
-
- err := auth.VerifyAccess(ctx.Context(), c.be,
- auth.AccessOptions{
- Readonly: c.readonly,
- Acl: parsedAcl,
- AclPermission: auth.PermissionWrite,
- IsRoot: isRoot,
- Acc: acct,
- Bucket: bucket,
- Object: keyStart,
- Action: auth.PutObjectAction,
- IsBucketPublic: IsBucketPublic,
- })
- if err != nil {
- return SendResponse(ctx, err,
- &MetaOpts{
- Logger: c.logger,
- MetricsMng: c.mm,
- Action: metrics.ActionUploadPart,
- BucketOwner: parsedAcl.Owner,
- })
- }
-
- contentLength, err := strconv.ParseInt(contentLengthStr, 10, 64)
- if err != nil {
- if c.debug {
- debuglogger.Logf("error parsing content length %q: %v",
- contentLengthStr, err)
- }
- return SendResponse(ctx, s3err.GetAPIError(s3err.ErrInvalidRequest),
- &MetaOpts{
- Logger: c.logger,
- MetricsMng: c.mm,
- Action: metrics.ActionUploadPart,
- BucketOwner: parsedAcl.Owner,
- })
- }
-
- algorithm, checksums, err := utils.ParseChecksumHeaders(ctx)
- if err != nil {
- if c.debug {
- debuglogger.Logf("err parsing checksum headers: %v", err)
- }
- return SendResponse(ctx, err,
- &MetaOpts{
- Logger: c.logger,
- MetricsMng: c.mm,
- Action: metrics.ActionPutObject,
- BucketOwner: parsedAcl.Owner,
- })
- }
-
- var body io.Reader
- bodyi := utils.ContextKeyBodyReader.Get(ctx)
- if bodyi != nil {
- body = bodyi.(io.Reader)
- } else {
- body = bytes.NewReader([]byte{})
- }
-
- res, err := c.be.UploadPart(ctx.Context(),
- &s3.UploadPartInput{
- Bucket: &bucket,
- Key: &keyStart,
- UploadId: &uploadId,
- PartNumber: &partNumber,
- ContentLength: &contentLength,
- Body: body,
- ChecksumAlgorithm: algorithm,
- ChecksumCRC32: backend.GetPtrFromString(checksums[types.ChecksumAlgorithmCrc32]),
- ChecksumCRC32C: backend.GetPtrFromString(checksums[types.ChecksumAlgorithmCrc32c]),
- ChecksumSHA1: backend.GetPtrFromString(checksums[types.ChecksumAlgorithmSha1]),
- ChecksumSHA256: backend.GetPtrFromString(checksums[types.ChecksumAlgorithmSha256]),
- ChecksumCRC64NVME: backend.GetPtrFromString(checksums[types.ChecksumAlgorithmCrc64nvme]),
- })
- if err == nil {
- headers := []utils.CustomHeader{}
- if res.ETag != nil {
- headers = append(headers, utils.CustomHeader{
- Key: "ETag",
- Value: *res.ETag,
- })
- }
- switch {
- case res.ChecksumCRC32 != nil:
- headers = append(headers, utils.CustomHeader{
- Key: "x-amz-checksum-crc32",
- Value: *res.ChecksumCRC32,
- })
- case res.ChecksumCRC32C != nil:
- headers = append(headers, utils.CustomHeader{
- Key: "x-amz-checksum-crc32c",
- Value: *res.ChecksumCRC32C,
- })
- case res.ChecksumCRC64NVME != nil:
- headers = append(headers, utils.CustomHeader{
- Key: "x-amz-checksum-crc64nvme",
- Value: *res.ChecksumCRC64NVME,
- })
- case res.ChecksumSHA1 != nil:
- headers = append(headers, utils.CustomHeader{
- Key: "x-amz-checksum-sha1",
- Value: *res.ChecksumSHA1,
- })
- case res.ChecksumSHA256 != nil:
- headers = append(headers, utils.CustomHeader{
- Key: "x-amz-checksum-sha256",
- Value: *res.ChecksumSHA256,
- })
- }
-
- utils.SetResponseHeaders(ctx, headers)
- }
- return SendResponse(ctx, err,
- &MetaOpts{
- Logger: c.logger,
- MetricsMng: c.mm,
- ContentLength: contentLength,
- Action: metrics.ActionUploadPart,
- BucketOwner: parsedAcl.Owner,
- })
- }
-
- if ctx.Request().URI().QueryArgs().Has("acl") {
- var input *s3.PutObjectAclInput
-
- if len(ctx.Body()) > 0 {
- if grants+acl != "" {
- if c.debug {
- debuglogger.Logf("invalid request: %q (grants) %q (acl)",
- grants, acl)
- }
- return SendResponse(ctx,
- s3err.GetAPIError(s3err.ErrInvalidRequest),
- &MetaOpts{
- Logger: c.logger,
- MetricsMng: c.mm,
- Action: metrics.ActionPutObjectAcl,
- BucketOwner: parsedAcl.Owner,
- })
- }
-
- var accessControlPolicy auth.AccessControlPolicy
- err := xml.Unmarshal(ctx.Body(), &accessControlPolicy)
- if err != nil {
- if c.debug {
- debuglogger.Logf("error unmarshalling access control policy: %v",
- err)
- }
- return SendResponse(ctx,
- s3err.GetAPIError(s3err.ErrInvalidRequest),
- &MetaOpts{
- Logger: c.logger,
- MetricsMng: c.mm,
- Action: metrics.ActionPutObjectAcl,
- BucketOwner: parsedAcl.Owner,
- })
- }
-
- //TODO: This part will be changed when object acls are implemented
-
- grants := []types.Grant{}
- for _, grt := range accessControlPolicy.AccessControlList.Grants {
- grants = append(grants, types.Grant{
- Grantee: &types.Grantee{
- ID: &grt.Grantee.ID,
- Type: grt.Grantee.Type,
- },
- Permission: types.Permission(grt.Permission),
- })
- }
-
- input = &s3.PutObjectAclInput{
- Bucket: &bucket,
- Key: &keyStart,
- ACL: "",
- AccessControlPolicy: &types.AccessControlPolicy{
- Owner: accessControlPolicy.Owner,
- Grants: grants,
- },
- }
- }
- if acl != "" {
- if acl != "private" && acl != "public-read" && acl != "public-read-write" {
- if c.debug {
- debuglogger.Logf("invalid acl: %q", acl)
- }
- return SendResponse(ctx,
- s3err.GetAPIError(s3err.ErrInvalidRequest),
- &MetaOpts{
- Logger: c.logger,
- MetricsMng: c.mm,
- Action: metrics.ActionPutObjectAcl,
- BucketOwner: parsedAcl.Owner,
- })
- }
- if len(ctx.Body()) > 0 || grants != "" {
- if c.debug {
- debuglogger.Logf("invalid request: %q (grants) %q (acl) %v (body len)",
- grants, acl, len(ctx.Body()))
- }
- return SendResponse(ctx,
- s3err.GetAPIError(s3err.ErrInvalidRequest),
- &MetaOpts{
- Logger: c.logger,
- MetricsMng: c.mm,
- Action: metrics.ActionPutObjectAcl,
- BucketOwner: parsedAcl.Owner,
- })
- }
-
- input = &s3.PutObjectAclInput{
- Bucket: &bucket,
- Key: &keyStart,
- ACL: types.ObjectCannedACL(acl),
- AccessControlPolicy: &types.AccessControlPolicy{
- Owner: &types.Owner{ID: &bucketOwner},
- },
- }
- }
- if grants != "" {
- input = &s3.PutObjectAclInput{
- Bucket: &bucket,
- Key: &keyStart,
- GrantFullControl: &grantFullControl,
- GrantRead: &grantRead,
- GrantReadACP: &grantReadACP,
- GrantWrite: &granWrite,
- GrantWriteACP: &grantWriteACP,
- AccessControlPolicy: &types.AccessControlPolicy{
- Owner: &types.Owner{ID: &bucketOwner},
- },
- ACL: "",
- }
- }
-
- err := c.be.PutObjectAcl(ctx.Context(), input)
- return SendResponse(ctx, err,
- &MetaOpts{
- Logger: c.logger,
- MetricsMng: c.mm,
- EvSender: c.evSender,
- Action: metrics.ActionPutObjectAcl,
- BucketOwner: parsedAcl.Owner,
- EventName: s3event.EventObjectAclPut,
- })
- }
-
- if copySource != "" {
- cs := copySource
- copySource, err := url.QueryUnescape(copySource)
- if err != nil {
- if c.debug {
- debuglogger.Logf("error unescaping copy source %q: %v",
- cs, err)
- }
- return SendXMLResponse(ctx, nil,
- s3err.GetAPIError(s3err.ErrInvalidCopySource),
- &MetaOpts{
- Logger: c.logger,
- MetricsMng: c.mm,
- Action: metrics.ActionCopyObject,
- BucketOwner: parsedAcl.Owner,
- })
- }
-
- err = auth.VerifyObjectCopyAccess(ctx.Context(), c.be, copySource,
- auth.AccessOptions{
- Acl: parsedAcl,
- AclPermission: auth.PermissionWrite,
- IsRoot: isRoot,
- Acc: acct,
- Bucket: bucket,
- Object: keyStart,
- Action: auth.PutObjectAction,
- })
- if err != nil {
- return SendXMLResponse(ctx, nil, err,
- &MetaOpts{
- Logger: c.logger,
- MetricsMng: c.mm,
- Action: metrics.ActionCopyObject,
- BucketOwner: parsedAcl.Owner,
- })
- }
-
- var mtime *time.Time
- var umtime *time.Time
- if copySrcModifSince != "" {
- tm, err := time.Parse(iso8601Format, copySrcModifSince)
- if err != nil {
- if c.debug {
- debuglogger.Logf("error parsing copy source modified since %q: %v",
- copySrcModifSince, err)
- }
- return SendXMLResponse(ctx, nil,
- s3err.GetAPIError(s3err.ErrInvalidCopySource),
- &MetaOpts{
- Logger: c.logger,
- MetricsMng: c.mm,
- Action: metrics.ActionCopyObject,
- BucketOwner: parsedAcl.Owner,
- })
- }
- mtime = &tm
- }
- if copySrcUnmodifSince != "" {
- tm, err := time.Parse(iso8601Format, copySrcUnmodifSince)
- if err != nil {
- if c.debug {
- debuglogger.Logf("error parsing copy source unmodified since %q: %v",
- copySrcUnmodifSince, err)
- }
- return SendXMLResponse(ctx, nil,
- s3err.GetAPIError(s3err.ErrInvalidCopySource),
- &MetaOpts{
- Logger: c.logger,
- MetricsMng: c.mm,
- Action: metrics.ActionCopyObject,
- BucketOwner: parsedAcl.Owner,
- })
- }
- umtime = &tm
- }
-
- metadata := utils.GetUserMetaData(&ctx.Request().Header)
-
- if directive != "" && directive != "COPY" && directive != "REPLACE" {
- if c.debug {
- debuglogger.Logf("invalid metadata directive: %v", directive)
- }
- return SendXMLResponse(ctx, nil,
- s3err.GetAPIError(s3err.ErrInvalidMetadataDirective),
- &MetaOpts{
- Logger: c.logger,
- MetricsMng: c.mm,
- Action: metrics.ActionCopyObject,
- BucketOwner: parsedAcl.Owner,
- })
- }
-
- metaDirective := types.MetadataDirectiveCopy
- if directive == "REPLACE" {
- metaDirective = types.MetadataDirectiveReplace
- }
-
- tDirective := types.TaggingDirective(ctx.Get("X-Amz-Tagging-Directive"))
- if tDirective != "" && tDirective != types.TaggingDirectiveCopy && tDirective != types.TaggingDirectiveReplace {
- if c.debug {
- debuglogger.Logf("invalid tagging direcrive: %v", tDirective)
- }
- return SendXMLResponse(ctx, nil,
- s3err.GetAPIError(s3err.ErrInvalidTaggingDirective),
- &MetaOpts{
- Logger: c.logger,
- MetricsMng: c.mm,
- Action: metrics.ActionCopyObject,
- BucketOwner: parsedAcl.Owner,
- })
- }
-
- taggingDirective := types.TaggingDirectiveCopy
- if tDirective == types.TaggingDirectiveReplace {
- taggingDirective = types.TaggingDirectiveReplace
- }
-
- checksumAlgorithm := types.ChecksumAlgorithm(ctx.Get("x-amz-checksum-algorithm"))
- err = utils.IsChecksumAlgorithmValid(checksumAlgorithm)
- if err != nil {
- return SendXMLResponse(ctx, nil, err,
- &MetaOpts{
- Logger: c.logger,
- MetricsMng: c.mm,
- Action: metrics.ActionCopyObject,
- BucketOwner: parsedAcl.Owner,
- })
- }
-
- objLock, err := utils.ParsObjectLockHdrs(ctx)
- if err != nil {
- return SendResponse(ctx, err,
- &MetaOpts{
- Logger: c.logger,
- MetricsMng: c.mm,
- Action: metrics.ActionPutObject,
- BucketOwner: parsedAcl.Owner,
- })
- }
-
- res, err := c.be.CopyObject(ctx.Context(),
- s3response.CopyObjectInput{
- Bucket: &bucket,
- Key: &keyStart,
- ContentType: &contentType,
- ContentDisposition: &contentDisposition,
- ContentEncoding: &contentEncoding,
- ContentLanguage: &contentLanguage,
- CacheControl: &cacheControl,
- Expires: &expires,
- Tagging: &tagging,
- TaggingDirective: taggingDirective,
- CopySource: ©Source,
- CopySourceIfMatch: ©SrcIfMatch,
- CopySourceIfNoneMatch: ©SrcIfNoneMatch,
- CopySourceIfModifiedSince: mtime,
- CopySourceIfUnmodifiedSince: umtime,
- ExpectedBucketOwner: &acct.Access,
- Metadata: metadata,
- MetadataDirective: metaDirective,
- StorageClass: types.StorageClass(storageClass),
- ChecksumAlgorithm: checksumAlgorithm,
- ObjectLockRetainUntilDate: &objLock.RetainUntilDate,
- ObjectLockLegalHoldStatus: objLock.LegalHoldStatus,
- ObjectLockMode: objLock.ObjectLockMode,
- })
- if err == nil {
- hdrs := []utils.CustomHeader{}
- if getstring(res.VersionId) != "" {
- hdrs = append(hdrs, utils.CustomHeader{
- Key: "x-amz-version-id",
- Value: getstring(res.VersionId),
- })
- }
- if getstring(res.CopySourceVersionId) != "" {
- hdrs = append(hdrs, utils.CustomHeader{
- Key: "x-amz-copy-source-version-id",
- Value: getstring(res.CopySourceVersionId),
- })
- }
- utils.SetResponseHeaders(ctx, hdrs)
-
- return SendXMLResponse(ctx, res.CopyObjectResult, err,
- &MetaOpts{
- Logger: c.logger,
- MetricsMng: c.mm,
- EvSender: c.evSender,
- Action: metrics.ActionCopyObject,
- BucketOwner: parsedAcl.Owner,
- ObjectETag: res.CopyObjectResult.ETag,
- VersionId: res.VersionId,
- EventName: s3event.EventObjectCreatedCopy,
- })
- } else {
- return SendXMLResponse(ctx, res, err,
- &MetaOpts{
- Logger: c.logger,
- MetricsMng: c.mm,
- Action: metrics.ActionCopyObject,
- BucketOwner: parsedAcl.Owner,
- })
- }
- }
-
- metadata := utils.GetUserMetaData(&ctx.Request().Header)
-
- err := auth.VerifyAccess(ctx.Context(), c.be,
- auth.AccessOptions{
- Readonly: c.readonly,
- Acl: parsedAcl,
- AclPermission: auth.PermissionWrite,
- IsRoot: isRoot,
- Acc: acct,
- Bucket: bucket,
- Object: keyStart,
- Action: auth.PutObjectAction,
- IsBucketPublic: IsBucketPublic,
- })
- if err != nil {
- return SendResponse(ctx, err,
- &MetaOpts{
- Logger: c.logger,
- MetricsMng: c.mm,
- Action: metrics.ActionPutObject,
- BucketOwner: parsedAcl.Owner,
- })
- }
-
- err = auth.CheckObjectAccess(ctx.Context(), bucket, acct.Access, []types.ObjectIdentifier{{Key: &keyStart}}, true, IsBucketPublic, c.be)
- if err != nil {
- return SendResponse(ctx, err,
- &MetaOpts{
- Logger: c.logger,
- MetricsMng: c.mm,
- Action: metrics.ActionPutObject,
- BucketOwner: parsedAcl.Owner,
- })
- }
-
- contentLength, err := strconv.ParseInt(contentLengthStr, 10, 64)
- if err != nil {
- if c.debug {
- debuglogger.Logf("error parsing content length %q: %v",
- contentLengthStr, err)
- }
- return SendResponse(ctx, s3err.GetAPIError(s3err.ErrInvalidRequest),
- &MetaOpts{
- Logger: c.logger,
- MetricsMng: c.mm,
- Action: metrics.ActionPutObject,
- BucketOwner: parsedAcl.Owner,
- })
- }
-
- objLock, err := utils.ParsObjectLockHdrs(ctx)
- if err != nil {
- return SendResponse(ctx, err,
- &MetaOpts{
- Logger: c.logger,
- MetricsMng: c.mm,
- Action: metrics.ActionPutObject,
- BucketOwner: parsedAcl.Owner,
- })
- }
-
- algorithm, checksums, err := utils.ParseChecksumHeaders(ctx)
- if err != nil {
- return SendResponse(ctx, err,
- &MetaOpts{
- Logger: c.logger,
- MetricsMng: c.mm,
- Action: metrics.ActionPutObject,
- BucketOwner: parsedAcl.Owner,
- })
- }
-
- var body io.Reader
- bodyi := utils.ContextKeyBodyReader.Get(ctx)
- if bodyi != nil {
- body = bodyi.(io.Reader)
- } else {
- body = bytes.NewReader([]byte{})
- }
-
- res, err := c.be.PutObject(ctx.Context(),
- s3response.PutObjectInput{
- Bucket: &bucket,
- Key: &keyStart,
- ContentLength: &contentLength,
- ContentType: &contentType,
- ContentEncoding: &contentEncoding,
- ContentDisposition: &contentDisposition,
- ContentLanguage: &contentLanguage,
- CacheControl: &cacheControl,
- Expires: &expires,
- Metadata: metadata,
- Body: body,
- Tagging: &tagging,
- ObjectLockRetainUntilDate: &objLock.RetainUntilDate,
- ObjectLockMode: objLock.ObjectLockMode,
- ObjectLockLegalHoldStatus: objLock.LegalHoldStatus,
- ChecksumAlgorithm: algorithm,
- ChecksumCRC32: backend.GetPtrFromString(checksums[types.ChecksumAlgorithmCrc32]),
- ChecksumCRC32C: backend.GetPtrFromString(checksums[types.ChecksumAlgorithmCrc32c]),
- ChecksumSHA1: backend.GetPtrFromString(checksums[types.ChecksumAlgorithmSha1]),
- ChecksumSHA256: backend.GetPtrFromString(checksums[types.ChecksumAlgorithmSha256]),
- ChecksumCRC64NVME: backend.GetPtrFromString(checksums[types.ChecksumAlgorithmCrc64nvme]),
- })
- if err != nil {
- return SendResponse(ctx, err,
- &MetaOpts{
- Logger: c.logger,
- MetricsMng: c.mm,
- ContentLength: contentLength,
- EvSender: c.evSender,
- Action: metrics.ActionPutObject,
- BucketOwner: parsedAcl.Owner,
- ObjectSize: contentLength,
- EventName: s3event.EventObjectCreatedPut,
- })
- }
- hdrs := []utils.CustomHeader{
- {
- Key: "ETag",
- Value: res.ETag,
- },
- }
-
- if res.VersionID != "" {
- hdrs = append(hdrs, utils.CustomHeader{
- Key: "x-amz-version-id",
- Value: res.VersionID,
- })
- }
- switch {
- case res.ChecksumCRC32 != nil:
- hdrs = append(hdrs, utils.CustomHeader{
- Key: "x-amz-checksum-crc32",
- Value: *res.ChecksumCRC32,
- })
- case res.ChecksumCRC32C != nil:
- hdrs = append(hdrs, utils.CustomHeader{
- Key: "x-amz-checksum-crc32c",
- Value: *res.ChecksumCRC32C,
- })
- case res.ChecksumCRC64NVME != nil:
- hdrs = append(hdrs, utils.CustomHeader{
- Key: "x-amz-checksum-crc64nvme",
- Value: *res.ChecksumCRC64NVME,
- })
- case res.ChecksumSHA1 != nil:
- hdrs = append(hdrs, utils.CustomHeader{
- Key: "x-amz-checksum-sha1",
- Value: *res.ChecksumSHA1,
- })
- case res.ChecksumSHA256 != nil:
- hdrs = append(hdrs, utils.CustomHeader{
- Key: "x-amz-checksum-sha256",
- Value: *res.ChecksumSHA256,
- })
- }
- if res.ChecksumType != "" {
- hdrs = append(hdrs, utils.CustomHeader{
- Key: "x-amz-checksum-type",
- Value: string(res.ChecksumType),
- })
- }
-
- utils.SetResponseHeaders(ctx, hdrs)
-
- return SendResponse(ctx, nil,
- &MetaOpts{
- Logger: c.logger,
- MetricsMng: c.mm,
- ContentLength: contentLength,
- EvSender: c.evSender,
- Action: metrics.ActionPutObject,
- BucketOwner: parsedAcl.Owner,
- ObjectETag: &res.ETag,
- ObjectSize: contentLength,
- EventName: s3event.EventObjectCreatedPut,
- })
-}
-
-func (c S3ApiController) DeleteBucket(ctx *fiber.Ctx) error {
- bucket := ctx.Params("bucket")
- acct := utils.ContextKeyAccount.Get(ctx).(auth.Account)
- isRoot := utils.ContextKeyIsRoot.Get(ctx).(bool)
- parsedAcl := utils.ContextKeyParsedAcl.Get(ctx).(auth.ACL)
- IsBucketPublic := utils.ContextKeyPublicBucket.IsSet(ctx)
-
- if ctx.Request().URI().QueryArgs().Has("tagging") {
- 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,
- })
- if err != nil {
- return SendResponse(ctx, err,
- &MetaOpts{
- Logger: c.logger,
- MetricsMng: c.mm,
- Action: metrics.ActionDeleteBucketTagging,
- BucketOwner: parsedAcl.Owner,
- })
- }
-
- err = c.be.DeleteBucketTagging(ctx.Context(), bucket)
- return SendResponse(ctx, err,
- &MetaOpts{
- Logger: c.logger,
- MetricsMng: c.mm,
- Action: metrics.ActionDeleteBucketTagging,
- BucketOwner: parsedAcl.Owner,
- Status: http.StatusNoContent,
- })
- }
-
- if ctx.Request().URI().QueryArgs().Has("ownershipControls") {
- 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.PutBucketOwnershipControlsAction,
- })
- if err != nil {
- return SendResponse(ctx, err,
- &MetaOpts{
- Logger: c.logger,
- MetricsMng: c.mm,
- Action: metrics.ActionDeleteBucketOwnershipControls,
- BucketOwner: parsedAcl.Owner,
- })
- }
-
- err = c.be.DeleteBucketOwnershipControls(ctx.Context(), bucket)
- return SendResponse(ctx, err,
- &MetaOpts{
- Logger: c.logger,
- MetricsMng: c.mm,
- Action: metrics.ActionDeleteBucketOwnershipControls,
- BucketOwner: parsedAcl.Owner,
- Status: http.StatusNoContent,
- })
- }
-
- if ctx.Request().URI().QueryArgs().Has("policy") {
- 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.DeleteBucketPolicyAction,
- })
- if err != nil {
- return SendResponse(ctx, err,
- &MetaOpts{
- Logger: c.logger,
- MetricsMng: c.mm,
- Action: metrics.ActionDeleteBucketPolicy,
- BucketOwner: parsedAcl.Owner,
- })
- }
-
- err = c.be.DeleteBucketPolicy(ctx.Context(), bucket)
- return SendResponse(ctx, err,
- &MetaOpts{
- Logger: c.logger,
- MetricsMng: c.mm,
- Action: metrics.ActionDeleteBucketPolicy,
- BucketOwner: parsedAcl.Owner,
- Status: http.StatusNoContent,
- })
- }
-
- if ctx.Request().URI().QueryArgs().Has("cors") {
- 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,
- })
- if err != nil {
- return SendResponse(ctx, err,
- &MetaOpts{
- Logger: c.logger,
- MetricsMng: c.mm,
- Action: metrics.ActionDeleteBucketCors,
- BucketOwner: parsedAcl.Owner,
- })
- }
-
- err = c.be.DeleteBucketCors(ctx.Context(), bucket)
- return SendResponse(ctx, err,
- &MetaOpts{
- Logger: c.logger,
- MetricsMng: c.mm,
- Action: metrics.ActionDeleteBucketCors,
- BucketOwner: parsedAcl.Owner,
- })
- }
-
- 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,
- })
- if err != nil {
- return SendResponse(ctx, err,
- &MetaOpts{
- Logger: c.logger,
- MetricsMng: c.mm,
- Action: metrics.ActionDeleteBucket,
- BucketOwner: parsedAcl.Owner,
- })
- }
-
- err = c.be.DeleteBucket(ctx.Context(), bucket)
- return SendResponse(ctx, err,
- &MetaOpts{
- Logger: c.logger,
- MetricsMng: c.mm,
- Action: metrics.ActionDeleteBucket,
- BucketOwner: parsedAcl.Owner,
- Status: http.StatusNoContent,
- })
-}
-
-func (c S3ApiController) DeleteObjects(ctx *fiber.Ctx) error {
- bucket := ctx.Params("bucket")
- acct := utils.ContextKeyAccount.Get(ctx).(auth.Account)
- isRoot := utils.ContextKeyIsRoot.Get(ctx).(bool)
- parsedAcl := utils.ContextKeyParsedAcl.Get(ctx).(auth.ACL)
- IsBucketPublic := utils.ContextKeyPublicBucket.IsSet(ctx)
- bypassHdr := ctx.Get("X-Amz-Bypass-Governance-Retention")
- var dObj s3response.DeleteObjects
-
- err := xml.Unmarshal(ctx.Body(), &dObj)
- if err != nil {
- if c.debug {
- debuglogger.Logf("error unmarshalling delete objects: %v", err)
- }
- return SendResponse(ctx, s3err.GetAPIError(s3err.ErrInvalidRequest),
- &MetaOpts{
- Logger: c.logger,
- MetricsMng: c.mm,
- Action: metrics.ActionDeleteObjects,
- BucketOwner: parsedAcl.Owner,
- })
- }
-
- 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,
- })
- if err != nil {
- return SendResponse(ctx, err,
- &MetaOpts{
- Logger: c.logger,
- MetricsMng: c.mm,
- Action: metrics.ActionDeleteObjects,
- BucketOwner: parsedAcl.Owner,
- })
- }
-
- // The AWS CLI sends 'True', while Go SDK sends 'true'
- bypass := strings.EqualFold(bypassHdr, "true")
-
- err = auth.CheckObjectAccess(ctx.Context(), bucket, acct.Access, dObj.Objects, bypass, IsBucketPublic, c.be)
- if err != nil {
- return SendResponse(ctx, err,
- &MetaOpts{
- Logger: c.logger,
- MetricsMng: c.mm,
- Action: metrics.ActionDeleteObjects,
- BucketOwner: parsedAcl.Owner,
- })
- }
-
- res, err := c.be.DeleteObjects(ctx.Context(),
- &s3.DeleteObjectsInput{
- Bucket: &bucket,
- Delete: &types.Delete{
- Objects: dObj.Objects,
- },
- })
- return SendXMLResponse(ctx, res, err,
- &MetaOpts{
- Logger: c.logger,
- MetricsMng: c.mm,
- Action: metrics.ActionDeleteObjects,
- ObjectCount: int64(len(dObj.Objects)),
- BucketOwner: parsedAcl.Owner,
- EvSender: c.evSender,
- EventName: s3event.EventObjectRemovedDeleteObjects,
- })
-}
-
-func (c S3ApiController) DeleteActions(ctx *fiber.Ctx) error {
- bucket := ctx.Params("bucket")
- key := ctx.Params("key")
- keyEnd := ctx.Params("*1")
- uploadId := ctx.Query("uploadId")
- versionId := ctx.Query("versionId")
- acct := utils.ContextKeyAccount.Get(ctx).(auth.Account)
- isRoot := utils.ContextKeyIsRoot.Get(ctx).(bool)
- IsBucketPublic := utils.ContextKeyPublicBucket.IsSet(ctx)
- parsedAcl := utils.ContextKeyParsedAcl.Get(ctx).(auth.ACL)
- bypassHdr := ctx.Get("X-Amz-Bypass-Governance-Retention")
-
- if keyEnd != "" {
- key = strings.Join([]string{key, keyEnd}, "/")
- }
- path := ctx.Path()
- if path[len(path)-1:] == "/" && key[len(key)-1:] != "/" {
- key = key + "/"
- }
-
- if ctx.Request().URI().QueryArgs().Has("tagging") {
- 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,
- })
- if err != nil {
- return SendResponse(ctx, err,
- &MetaOpts{
- Logger: c.logger,
- MetricsMng: c.mm,
- Action: metrics.ActionDeleteObjectTagging,
- BucketOwner: parsedAcl.Owner,
- })
- }
-
- err = c.be.DeleteObjectTagging(ctx.Context(), bucket, key)
- return SendResponse(ctx, err,
- &MetaOpts{
- Status: http.StatusNoContent,
- Logger: c.logger,
- MetricsMng: c.mm,
- EvSender: c.evSender,
- Action: metrics.ActionDeleteObjectTagging,
- BucketOwner: parsedAcl.Owner,
- EventName: s3event.EventObjectTaggingDelete,
- })
- }
-
- if uploadId != "" {
- expectedBucketOwner := ctx.Get("X-Amz-Expected-Bucket-Owner")
- requestPayer := ctx.Get("X-Amz-Request-Payer")
-
- 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,
- })
- if err != nil {
- return SendResponse(ctx, err,
- &MetaOpts{
- Logger: c.logger,
- MetricsMng: c.mm,
- Action: metrics.ActionAbortMultipartUpload,
- BucketOwner: parsedAcl.Owner,
- })
- }
-
- err = c.be.AbortMultipartUpload(ctx.Context(),
- &s3.AbortMultipartUploadInput{
- UploadId: &uploadId,
- Bucket: &bucket,
- Key: &key,
- ExpectedBucketOwner: &expectedBucketOwner,
- RequestPayer: types.RequestPayer(requestPayer),
- })
- return SendResponse(ctx, err,
- &MetaOpts{
- Logger: c.logger,
- MetricsMng: c.mm,
- Action: metrics.ActionAbortMultipartUpload,
- BucketOwner: parsedAcl.Owner,
- Status: http.StatusNoContent,
- })
- }
-
- //TODO: check s3:DeleteObjectVersion policy in case a use tries to delete a version of an object
-
- 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,
- })
- if err != nil {
- return SendResponse(ctx, err,
- &MetaOpts{
- Logger: c.logger,
- MetricsMng: c.mm,
- Action: metrics.ActionDeleteObject,
- BucketOwner: parsedAcl.Owner,
- })
- }
-
- // The AWS CLI sends 'True', while Go SDK sends 'true'
- bypass := strings.EqualFold(bypassHdr, "true")
-
- err = auth.CheckObjectAccess(ctx.Context(), bucket, acct.Access, []types.ObjectIdentifier{{Key: &key, VersionId: &versionId}}, bypass, IsBucketPublic, c.be)
- if err != nil {
- return SendResponse(ctx, err,
- &MetaOpts{
- Logger: c.logger,
- MetricsMng: c.mm,
- Action: metrics.ActionDeleteObject,
- BucketOwner: parsedAcl.Owner,
- })
- }
-
- res, err := c.be.DeleteObject(ctx.Context(),
- &s3.DeleteObjectInput{
- Bucket: &bucket,
- Key: &key,
- VersionId: &versionId,
- })
- if err != nil {
- return SendResponse(ctx, err,
- &MetaOpts{
- Logger: c.logger,
- MetricsMng: c.mm,
- EvSender: c.evSender,
- Action: metrics.ActionDeleteObject,
- BucketOwner: parsedAcl.Owner,
- EventName: s3event.EventObjectRemovedDelete,
- Status: http.StatusNoContent,
- })
- }
-
- hdrs := []utils.CustomHeader{}
- if res.VersionId != nil && *res.VersionId != "" {
- hdrs = append(hdrs, utils.CustomHeader{
- Key: "x-amz-version-id",
- Value: *res.VersionId,
- })
- }
- if res.DeleteMarker != nil && *res.DeleteMarker {
- hdrs = append(hdrs, utils.CustomHeader{
- Key: "x-amz-delete-marker",
- Value: "true",
- })
- }
-
- utils.SetResponseHeaders(ctx, hdrs)
-
- return SendResponse(ctx, nil,
- &MetaOpts{
- Logger: c.logger,
- MetricsMng: c.mm,
- EvSender: c.evSender,
- Action: metrics.ActionDeleteObject,
- BucketOwner: parsedAcl.Owner,
- EventName: s3event.EventObjectRemovedDelete,
- Status: http.StatusNoContent,
- })
-}
-
-func (c S3ApiController) HeadBucket(ctx *fiber.Ctx) error {
- bucket := ctx.Params("bucket")
- acct := utils.ContextKeyAccount.Get(ctx).(auth.Account)
- isRoot := utils.ContextKeyIsRoot.Get(ctx).(bool)
- region := utils.ContextKeyRegion.Get(ctx).(string)
- parsedAcl := utils.ContextKeyParsedAcl.Get(ctx).(auth.ACL)
- 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,
- Action: auth.ListBucketAction,
- IsBucketPublic: isPublicBucket,
- })
- if err != nil {
- return SendResponse(ctx, err,
- &MetaOpts{
- Logger: c.logger,
- MetricsMng: c.mm,
- Action: metrics.ActionHeadBucket,
- BucketOwner: parsedAcl.Owner,
- })
- }
-
- _, err = c.be.HeadBucket(ctx.Context(),
- &s3.HeadBucketInput{
- Bucket: &bucket,
- })
-
- utils.SetResponseHeaders(ctx, []utils.CustomHeader{
- {
- Key: "X-Amz-Access-Point-Alias",
- Value: "false",
- },
- {
- Key: "X-Amz-Bucket-Region",
- Value: region,
- },
- })
- return SendResponse(ctx, err,
- &MetaOpts{
- Logger: c.logger,
- MetricsMng: c.mm,
- Action: metrics.ActionHeadBucket,
- BucketOwner: parsedAcl.Owner,
- })
-}
-
-const (
- timefmt = "Mon, 02 Jan 2006 15:04:05 GMT"
-)
-
-func (c S3ApiController) HeadObject(ctx *fiber.Ctx) error {
- bucket := ctx.Params("bucket")
- 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)
- partNumberQuery := int32(ctx.QueryInt("partNumber", -1))
- versionId := ctx.Query("versionId")
- objRange := ctx.Get("Range")
- key := ctx.Params("key")
- keyEnd := ctx.Params("*1")
- if keyEnd != "" {
- key = strings.Join([]string{key, keyEnd}, "/")
- }
- path := ctx.Path()
- if path[len(path)-1:] == "/" && key[len(key)-1:] != "/" {
- key = key + "/"
- }
-
- var partNumber *int32
- if ctx.Request().URI().QueryArgs().Has("partNumber") {
- if partNumberQuery < 1 || partNumberQuery > 10000 {
- if c.debug {
- debuglogger.Logf("invalid part number: %d", partNumberQuery)
- }
- return SendResponse(ctx, s3err.GetAPIError(s3err.ErrInvalidPartNumber),
- &MetaOpts{
- Logger: c.logger,
- MetricsMng: c.mm,
- Action: metrics.ActionHeadObject,
- BucketOwner: parsedAcl.Owner,
- })
- }
-
- partNumber = &partNumberQuery
- }
-
- 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: isPublicBucket,
- })
- if err != nil {
- return SendResponse(ctx, err,
- &MetaOpts{
- Logger: c.logger,
- MetricsMng: c.mm,
- Action: metrics.ActionHeadObject,
- BucketOwner: parsedAcl.Owner,
- })
- }
-
- checksumMode := types.ChecksumMode(ctx.Get("x-amz-checksum-mode"))
- if checksumMode != "" && checksumMode != types.ChecksumModeEnabled {
- if c.debug {
- debuglogger.Logf("invalid x-amz-checksum-mode header value: %v", checksumMode)
- }
- return SendResponse(ctx, s3err.GetInvalidChecksumHeaderErr("x-amz-checksum-mode"),
- &MetaOpts{
- Logger: c.logger,
- MetricsMng: c.mm,
- Action: metrics.ActionHeadObject,
- BucketOwner: parsedAcl.Owner,
- })
- }
-
- res, err := c.be.HeadObject(ctx.Context(),
- &s3.HeadObjectInput{
- Bucket: &bucket,
- Key: &key,
- PartNumber: partNumber,
- VersionId: &versionId,
- ChecksumMode: checksumMode,
- Range: &objRange,
- })
- if err != nil {
- if res != nil {
- utils.SetResponseHeaders(ctx, []utils.CustomHeader{
- {
- Key: "x-amz-delete-marker",
- Value: "true",
- },
- {
- Key: "Last-Modified",
- Value: res.LastModified.UTC().Format(timefmt),
- },
- })
- }
- return SendResponse(ctx, err,
- &MetaOpts{
- Logger: c.logger,
- MetricsMng: c.mm,
- Action: metrics.ActionHeadObject,
- BucketOwner: parsedAcl.Owner,
- })
- }
-
- utils.SetMetaHeaders(ctx, res.Metadata)
- headers := []utils.CustomHeader{
- {
- Key: "Content-Length",
- Value: fmt.Sprint(getint64(res.ContentLength)),
- },
- {
- Key: "ETag",
- Value: getstring(res.ETag),
- },
- {
- Key: "x-amz-restore",
- Value: getstring(res.Restore),
- },
- }
- if getstring(res.AcceptRanges) != "" {
- headers = append(headers, utils.CustomHeader{
- Key: "accept-ranges",
- Value: getstring(res.AcceptRanges),
- })
- }
- if getstring(res.ContentRange) != "" {
- headers = append(headers, utils.CustomHeader{
- Key: "Content-Range",
- Value: getstring(res.ContentRange),
- })
- }
- if getstring(res.ContentDisposition) != "" {
- headers = append(headers, utils.CustomHeader{
- Key: "Content-Disposition",
- Value: getstring(res.ContentDisposition),
- })
- }
- if getstring(res.ContentEncoding) != "" {
- headers = append(headers, utils.CustomHeader{
- Key: "Content-Encoding",
- Value: getstring(res.ContentEncoding),
- })
- }
- if getstring(res.ContentLanguage) != "" {
- headers = append(headers, utils.CustomHeader{
- Key: "Content-Language",
- Value: getstring(res.ContentLanguage),
- })
- }
- if getstring(res.CacheControl) != "" {
- headers = append(headers, utils.CustomHeader{
- Key: "Cache-Control",
- Value: getstring(res.CacheControl),
- })
- }
- if getstring(res.ExpiresString) != "" {
- headers = append(headers, utils.CustomHeader{
- Key: "Expires",
- Value: getstring(res.ExpiresString),
- })
- }
- if res.ObjectLockMode != "" {
- headers = append(headers, utils.CustomHeader{
- Key: "x-amz-object-lock-mode",
- Value: string(res.ObjectLockMode),
- })
- }
- if res.ObjectLockLegalHoldStatus != "" {
- headers = append(headers, utils.CustomHeader{
- Key: "x-amz-object-lock-legal-hold",
- Value: string(res.ObjectLockLegalHoldStatus),
- })
- }
- if res.ObjectLockRetainUntilDate != nil {
- retainUntilDate := res.ObjectLockRetainUntilDate.Format(time.RFC3339)
- headers = append(headers, utils.CustomHeader{
- Key: "x-amz-object-lock-retain-until-date",
- Value: retainUntilDate,
- })
- }
- if res.PartsCount != nil {
- headers = append(headers, utils.CustomHeader{
- Key: "x-amz-mp-parts-count",
- Value: fmt.Sprintf("%v", *res.PartsCount),
- })
- }
- if res.LastModified != nil {
- lastmod := res.LastModified.UTC().Format(timefmt)
- headers = append(headers, utils.CustomHeader{
- Key: "Last-Modified",
- Value: lastmod,
- })
- }
- if res.StorageClass != "" {
- headers = append(headers, utils.CustomHeader{
- Key: "x-amz-storage-class",
- Value: string(res.StorageClass),
- })
- }
- switch {
- case res.ChecksumCRC32 != nil:
- headers = append(headers, utils.CustomHeader{
- Key: "x-amz-checksum-crc32",
- Value: *res.ChecksumCRC32,
- })
- case res.ChecksumCRC32C != nil:
- headers = append(headers, utils.CustomHeader{
- Key: "x-amz-checksum-crc32c",
- Value: *res.ChecksumCRC32C,
- })
- case res.ChecksumCRC64NVME != nil:
- headers = append(headers, utils.CustomHeader{
- Key: "x-amz-checksum-crc64nvme",
- Value: *res.ChecksumCRC64NVME,
- })
- case res.ChecksumSHA1 != nil:
- headers = append(headers, utils.CustomHeader{
- Key: "x-amz-checksum-sha1",
- Value: *res.ChecksumSHA1,
- })
- case res.ChecksumSHA256 != nil:
- headers = append(headers, utils.CustomHeader{
- Key: "x-amz-checksum-sha256",
- Value: *res.ChecksumSHA256,
- })
- }
- if res.ChecksumType != "" {
- headers = append(headers, utils.CustomHeader{
- Key: "x-amz-checksum-type",
- Value: string(res.ChecksumType),
- })
- }
-
- contentType := getstring(res.ContentType)
- if contentType == "" {
- contentType = defaultContentType
- }
- headers = append(headers, utils.CustomHeader{
- Key: "Content-Type",
- Value: contentType,
- })
-
- if getstring(res.VersionId) != "" {
- headers = append(headers, utils.CustomHeader{
- Key: "x-amz-version-id",
- Value: getstring(res.VersionId),
- })
- }
-
- utils.SetResponseHeaders(ctx, headers)
-
- return SendResponse(ctx, nil,
- &MetaOpts{
- Logger: c.logger,
- MetricsMng: c.mm,
- Action: metrics.ActionHeadObject,
- BucketOwner: parsedAcl.Owner,
- })
-}
-
-func (c S3ApiController) CreateActions(ctx *fiber.Ctx) error {
- bucket := ctx.Params("bucket")
- key := ctx.Params("key")
- keyEnd := ctx.Params("*1")
- uploadId := ctx.Query("uploadId")
- acct := utils.ContextKeyAccount.Get(ctx).(auth.Account)
- isRoot := utils.ContextKeyIsRoot.Get(ctx).(bool)
- IsBucketPublic := utils.ContextKeyPublicBucket.IsSet(ctx)
- parsedAcl := utils.ContextKeyParsedAcl.Get(ctx).(auth.ACL)
- contentType := ctx.Get("Content-Type")
- contentDisposition := ctx.Get("Content-Disposition")
- contentLanguage := ctx.Get("Content-Language")
- cacheControl := ctx.Get("Cache-Control")
- contentEncoding := ctx.Get("Content-Encoding")
- tagging := ctx.Get("X-Amz-Tagging")
-
- if keyEnd != "" {
- key = strings.Join([]string{key, keyEnd}, "/")
- }
-
- path := ctx.Path()
- if path[len(path)-1:] == "/" && key[len(key)-1:] != "/" {
- key = key + "/"
- }
-
- if ctx.Request().URI().QueryArgs().Has("restore") {
- var restoreRequest types.RestoreRequest
- if err := xml.Unmarshal(ctx.Body(), &restoreRequest); err != nil {
- if !errors.Is(err, io.EOF) {
- if c.debug {
- debuglogger.Logf("failed to parse the request body: %v", err)
- }
- return SendResponse(ctx, s3err.GetAPIError(s3err.ErrMalformedXML),
- &MetaOpts{
- Logger: c.logger,
- MetricsMng: c.mm,
- Action: metrics.ActionRestoreObject,
- BucketOwner: parsedAcl.Owner,
- })
- }
- }
- 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,
- })
- if err != nil {
- return SendResponse(ctx, err,
- &MetaOpts{
- Logger: c.logger,
- MetricsMng: c.mm,
- Action: metrics.ActionRestoreObject,
- BucketOwner: parsedAcl.Owner,
- })
- }
-
- err = c.be.RestoreObject(ctx.Context(), &s3.RestoreObjectInput{
- Bucket: &bucket,
- Key: &key,
- RestoreRequest: &restoreRequest,
- })
- return SendResponse(ctx, err,
- &MetaOpts{
- Logger: c.logger,
- MetricsMng: c.mm,
- EvSender: c.evSender,
- Action: metrics.ActionRestoreObject,
- BucketOwner: parsedAcl.Owner,
- EventName: s3event.EventObjectRestoreCompleted,
- })
- }
-
- if ctx.Request().URI().QueryArgs().Has("select") && ctx.Query("select-type") == "2" {
- var payload s3response.SelectObjectContentPayload
-
- err := xml.Unmarshal(ctx.Body(), &payload)
- if err != nil {
- if c.debug {
- debuglogger.Logf("error unmarshalling select object content: %v", err)
- }
- return SendXMLResponse(ctx, nil,
- s3err.GetAPIError(s3err.ErrMalformedXML),
- &MetaOpts{
- Logger: c.logger,
- MetricsMng: c.mm,
- Action: metrics.ActionSelectObjectContent,
- BucketOwner: parsedAcl.Owner,
- })
- }
-
- 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,
- })
- if err != nil {
- return SendXMLResponse(ctx, nil, err,
- &MetaOpts{
- Logger: c.logger,
- MetricsMng: c.mm,
- Action: metrics.ActionSelectObjectContent,
- BucketOwner: parsedAcl.Owner,
- })
- }
-
- sw := c.be.SelectObjectContent(ctx.Context(),
- &s3.SelectObjectContentInput{
- Bucket: &bucket,
- Key: &key,
- Expression: payload.Expression,
- ExpressionType: payload.ExpressionType,
- InputSerialization: payload.InputSerialization,
- OutputSerialization: payload.OutputSerialization,
- RequestProgress: payload.RequestProgress,
- ScanRange: payload.ScanRange,
- })
-
- ctx.Context().SetBodyStreamWriter(sw)
-
- return nil
- }
-
- if uploadId != "" {
- data := struct {
- Parts []types.CompletedPart `xml:"Part"`
- }{}
-
- err := xml.Unmarshal(ctx.Body(), &data)
- if err != nil {
- if c.debug {
- debuglogger.Logf("error unmarshalling complete multipart upload: %v", err)
- }
- return SendXMLResponse(ctx, nil,
- s3err.GetAPIError(s3err.ErrMalformedXML),
- &MetaOpts{
- Logger: c.logger,
- MetricsMng: c.mm,
- Action: metrics.ActionCompleteMultipartUpload,
- BucketOwner: parsedAcl.Owner,
- })
- }
-
- if len(data.Parts) == 0 {
- if c.debug {
- debuglogger.Logf("empty parts provided for complete multipart upload")
- }
- return SendXMLResponse(ctx, nil,
- s3err.GetAPIError(s3err.ErrEmptyParts),
- &MetaOpts{
- Logger: c.logger,
- MetricsMng: c.mm,
- Action: metrics.ActionCompleteMultipartUpload,
- BucketOwner: parsedAcl.Owner,
- })
- }
-
- var mpuObjectSize *int64
- mpuObjSizeHdr := ctx.Get("X-Amz-Mp-Object-Size")
- if mpuObjSizeHdr != "" {
- val, err := strconv.ParseInt(mpuObjSizeHdr, 10, 64)
- //TODO: Not sure if invalid request should be returned
- if err != nil {
- if c.debug {
- debuglogger.Logf("invalid value for 'x-amz-mp-objects-size' header: %v", err)
- }
- return SendXMLResponse(ctx, nil,
- s3err.GetAPIError(s3err.ErrInvalidRequest),
- &MetaOpts{
- Logger: c.logger,
- MetricsMng: c.mm,
- Action: metrics.ActionCompleteMultipartUpload,
- BucketOwner: parsedAcl.Owner,
- })
- }
-
- if val < 0 {
- debuglogger.Logf("value for 'x-amz-mp-objects-size' header is less than 0: %v", val)
- return SendXMLResponse(ctx, nil,
- s3err.GetInvalidMpObjectSizeErr(val),
- &MetaOpts{
- Logger: c.logger,
- MetricsMng: c.mm,
- Action: metrics.ActionCompleteMultipartUpload,
- BucketOwner: parsedAcl.Owner,
- })
- }
-
- mpuObjectSize = &val
- }
-
- 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,
- })
- if err != nil {
- return SendXMLResponse(ctx, nil, err,
- &MetaOpts{
- Logger: c.logger,
- MetricsMng: c.mm,
- Action: metrics.ActionCompleteMultipartUpload,
- BucketOwner: parsedAcl.Owner,
- })
- }
-
- _, checksums, err := utils.ParseChecksumHeaders(ctx)
- if err != nil {
- return SendXMLResponse(ctx, nil, err,
- &MetaOpts{
- Logger: c.logger,
- MetricsMng: c.mm,
- Action: metrics.ActionCompleteMultipartUpload,
- BucketOwner: parsedAcl.Owner,
- })
- }
-
- checksumType := types.ChecksumType(ctx.Get("x-amz-checksum-type"))
- err = utils.IsChecksumTypeValid(checksumType)
- if err != nil {
- return SendXMLResponse(ctx, nil, err,
- &MetaOpts{
- Logger: c.logger,
- MetricsMng: c.mm,
- Action: metrics.ActionCompleteMultipartUpload,
- BucketOwner: parsedAcl.Owner,
- })
- }
-
- res, versid, err := c.be.CompleteMultipartUpload(ctx.Context(),
- &s3.CompleteMultipartUploadInput{
- Bucket: &bucket,
- Key: &key,
- UploadId: &uploadId,
- MultipartUpload: &types.CompletedMultipartUpload{
- Parts: data.Parts,
- },
- MpuObjectSize: mpuObjectSize,
- ChecksumCRC32: backend.GetPtrFromString(checksums[types.ChecksumAlgorithmCrc32]),
- ChecksumCRC32C: backend.GetPtrFromString(checksums[types.ChecksumAlgorithmCrc32c]),
- ChecksumSHA1: backend.GetPtrFromString(checksums[types.ChecksumAlgorithmSha1]),
- ChecksumSHA256: backend.GetPtrFromString(checksums[types.ChecksumAlgorithmSha256]),
- ChecksumCRC64NVME: backend.GetPtrFromString(checksums[types.ChecksumAlgorithmCrc64nvme]),
- ChecksumType: checksumType,
- })
- if err == nil {
- if versid != "" {
- utils.SetResponseHeaders(ctx, []utils.CustomHeader{
- {
- Key: "x-amz-version-id",
- Value: versid,
- },
- })
- }
- return SendXMLResponse(ctx, res, err,
- &MetaOpts{
- Logger: c.logger,
- MetricsMng: c.mm,
- EvSender: c.evSender,
- Action: metrics.ActionCompleteMultipartUpload,
- BucketOwner: parsedAcl.Owner,
- ObjectETag: res.ETag,
- EventName: s3event.EventCompleteMultipartUpload,
- VersionId: backend.GetPtrFromString(versid),
- })
- }
- return SendXMLResponse(ctx, res, err,
- &MetaOpts{
- Logger: c.logger,
- MetricsMng: c.mm,
- Action: metrics.ActionCompleteMultipartUpload,
- BucketOwner: parsedAcl.Owner,
- })
- }
-
- 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,
- })
- if err != nil {
- return SendXMLResponse(ctx, nil, err,
- &MetaOpts{
- Logger: c.logger,
- MetricsMng: c.mm,
- Action: metrics.ActionCreateMultipartUpload,
- BucketOwner: parsedAcl.Owner,
- })
- }
-
- objLockState, err := utils.ParsObjectLockHdrs(ctx)
- if err != nil {
- return SendXMLResponse(ctx, nil, err,
- &MetaOpts{
- Logger: c.logger,
- MetricsMng: c.mm,
- Action: metrics.ActionCreateMultipartUpload,
- BucketOwner: parsedAcl.Owner,
- })
- }
-
- metadata := utils.GetUserMetaData(&ctx.Request().Header)
-
- checksumAlgorithm, checksumType, err := utils.ParseCreateMpChecksumHeaders(ctx)
- if err != nil {
- return SendXMLResponse(ctx, nil, err,
- &MetaOpts{
- Logger: c.logger,
- MetricsMng: c.mm,
- Action: metrics.ActionCreateMultipartUpload,
- BucketOwner: parsedAcl.Owner,
- })
- }
-
- expires := ctx.Get("Expires")
-
- res, err := c.be.CreateMultipartUpload(ctx.Context(),
- s3response.CreateMultipartUploadInput{
- Bucket: &bucket,
- Key: &key,
- Tagging: &tagging,
- ContentType: &contentType,
- ContentEncoding: &contentEncoding,
- ContentDisposition: &contentDisposition,
- ContentLanguage: &contentLanguage,
- CacheControl: &cacheControl,
- Expires: &expires,
- ObjectLockRetainUntilDate: &objLockState.RetainUntilDate,
- ObjectLockMode: objLockState.ObjectLockMode,
- ObjectLockLegalHoldStatus: objLockState.LegalHoldStatus,
- Metadata: metadata,
- ChecksumAlgorithm: checksumAlgorithm,
- ChecksumType: checksumType,
- })
- if err == nil {
- if checksumAlgorithm != "" {
- utils.SetResponseHeaders(ctx, []utils.CustomHeader{
- {
- Key: "x-amz-checksum-algorithm",
- Value: string(checksumAlgorithm),
- },
- })
- }
- }
- return SendXMLResponse(ctx, res, err,
- &MetaOpts{
- Logger: c.logger,
- MetricsMng: c.mm,
- Action: metrics.ActionCreateMultipartUpload,
- BucketOwner: parsedAcl.Owner,
- })
-}
-
-type MetaOpts struct {
- Logger s3log.AuditLogger
- EvSender s3event.S3EventSender
- MetricsMng *metrics.Manager
+// MetaOptions holds the metadata for metrics, audit logs and s3 events
+type MetaOptions struct {
ContentLength int64
- Action string
BucketOwner string
ObjectSize int64
ObjectCount int64
@@ -3903,74 +92,111 @@ type MetaOpts struct {
Status int
}
-func SendResponse(ctx *fiber.Ctx, err error, l *MetaOpts) error {
- if l.Logger != nil {
- l.Logger.Log(ctx, err, nil, s3log.LogMeta{
- Action: l.Action,
- BucketOwner: l.BucketOwner,
- ObjectSize: l.ObjectSize,
- })
- }
- if l.MetricsMng != nil {
- if l.ObjectCount > 0 {
- l.MetricsMng.Send(ctx, err, l.Action, l.ObjectCount, l.Status)
- } else {
- l.MetricsMng.Send(ctx, err, l.Action, l.ContentLength, l.Status)
- }
- }
- if err != nil {
- var apierr s3err.APIError
- if errors.As(err, &apierr) {
- ctx.Status(apierr.HTTPStatusCode)
- return ctx.Send(s3err.GetAPIErrorResponse(apierr, "", "", ""))
- }
-
- fmt.Fprintf(os.Stderr, "Internal Error, %v\n", err)
- ctx.Status(http.StatusInternalServerError)
- return ctx.Send(s3err.GetAPIErrorResponse(
- s3err.GetAPIError(s3err.ErrInternalError), "", "", ""))
- }
- if l.EvSender != nil {
- l.EvSender.SendEvent(ctx, s3event.EventMeta{
- ObjectSize: l.ObjectSize,
- ObjectETag: l.ObjectETag,
- EventName: l.EventName,
- BucketOwner: l.BucketOwner,
- VersionId: l.VersionId,
- })
- }
-
- if l.Status == 0 {
- l.Status = http.StatusOK
- }
- // https://github.com/gofiber/fiber/issues/2080
- // ctx.SendStatus() sets incorrect content length on HEAD request
- ctx.Status(l.Status)
- return nil
+// Response is the type definition for a controller response
+// Data - Response body
+// Headers - Resposne headers
+// MetaOpts - Meta options for metrics, audit logs and s3 events
+type Response struct {
+ Data any
+ Headers map[string]*string
+ MetaOpts *MetaOptions
}
-var (
- xmlhdr = []byte(`` + "\n")
-)
+// Services groups the metrics manager, s3 event sender and audit logger
+type Services struct {
+ Logger s3log.AuditLogger
+ EventSender s3event.S3EventSender
+ MetricsManager metrics.Manager
+}
-const (
- maxXMLBodyLen = 4 * 1024 * 1024
-)
+// Controller is the type definition for an s3api controller
+type Controller func(ctx *fiber.Ctx) (*Response, error)
-func SendXMLResponse(ctx *fiber.Ctx, resp any, err error, l *MetaOpts) error {
- if l.MetricsMng != nil {
- if l.ObjectCount > 0 {
- l.MetricsMng.Send(ctx, err, l.Action, l.ObjectCount, l.Status)
+// ProcessHandlers groups a controller and multiple middlewares into a single fiber handler
+func ProcessHandlers(controller Controller, s3action string, svc *Services, handlers ...fiber.Handler) fiber.Handler {
+ return func(ctx *fiber.Ctx) error {
+ // if skip locals is set, skip to the next rout handler
+ if utils.ContextKeySkip.IsSet(ctx) {
+ utils.ContextKeySkip.Delete(ctx)
+ return ctx.Next()
+ }
+
+ for _, handler := range handlers {
+ err := handler(ctx)
+ if err != nil {
+ return ProcessController(ctx, func(ctx *fiber.Ctx) (*Response, error) {
+ return &Response{
+ MetaOpts: &MetaOptions{},
+ }, err
+ }, s3action, svc)
+ }
+ }
+
+ return ProcessController(ctx, controller, s3action, svc)
+ }
+}
+
+// WrapMiddleware executes the given middleware and handles sending the audit logs
+// and metrics. It also handles the error parsing
+func WrapMiddleware(handler fiber.Handler, logger s3log.AuditLogger, mm metrics.Manager) fiber.Handler {
+ return func(ctx *fiber.Ctx) error {
+ err := handler(ctx)
+ if err != nil {
+ if mm != nil {
+ mm.Send(ctx, err, metrics.ActionUndetected, 0, 0)
+ }
+ if logger != nil {
+ logger.Log(ctx, err, ctx.Body(), s3log.LogMeta{
+ Action: metrics.ActionUndetected,
+ })
+ }
+
+ serr, ok := err.(s3err.APIError)
+ if ok {
+ ctx.Status(serr.HTTPStatusCode)
+ return ctx.Send(s3err.GetAPIErrorResponse(serr, "", "", ""))
+ }
+
+ debuglogger.Logf("Internal Error, %v", err)
+ ctx.Status(http.StatusInternalServerError)
+
+ // If the error is not 's3err.APIError' return 'InternalError'
+ return ctx.Send(s3err.GetAPIErrorResponse(
+ s3err.GetAPIError(s3err.ErrInternalError), "", "", ""))
+ }
+
+ return ctx.Next()
+ }
+}
+
+// ProcessController executes the given s3api controller and handles the metrics
+// access logs and s3 events
+func ProcessController(ctx *fiber.Ctx, controller Controller, s3action string, svc *Services) error {
+ response, err := controller(ctx)
+
+ // Set the response headers
+ SetResponseHeaders(ctx, response.Headers)
+
+ opts := response.MetaOpts
+ if opts == nil {
+ opts = &MetaOptions{}
+ }
+ // Send the metrics
+ if svc.MetricsManager != nil {
+ if opts.ObjectCount > 0 {
+ svc.MetricsManager.Send(ctx, err, s3action, opts.ObjectCount, opts.Status)
} else {
- l.MetricsMng.Send(ctx, err, l.Action, l.ContentLength, l.Status)
+ svc.MetricsManager.Send(ctx, err, s3action, opts.ContentLength, opts.Status)
}
}
+ // Handle the error case
if err != nil {
- if l.Logger != nil {
- l.Logger.Log(ctx, err, nil, s3log.LogMeta{
- Action: l.Action,
- BucketOwner: l.BucketOwner,
- ObjectSize: l.ObjectSize,
+ // Audit the error log
+ if svc.Logger != nil {
+ svc.Logger.Log(ctx, err, nil, s3log.LogMeta{
+ Action: s3action,
+ BucketOwner: opts.BucketOwner,
+ ObjectSize: opts.ObjectSize,
})
}
serr, ok := err.(s3err.APIError)
@@ -3982,56 +208,66 @@ func SendXMLResponse(ctx *fiber.Ctx, resp any, err error, l *MetaOpts) error {
fmt.Fprintf(os.Stderr, "Internal Error, %v\n", err)
ctx.Status(http.StatusInternalServerError)
+ // If the error is not 's3err.APIError' return 'InternalError'
return ctx.Send(s3err.GetAPIErrorResponse(
s3err.GetAPIError(s3err.ErrInternalError), "", "", ""))
}
- var b []byte
-
- // Handle already encoded responses(text, json...)
- encodedResp, ok := resp.([]byte)
- if ok {
- b = encodedResp
+ if opts.Status == 0 {
+ opts.Status = http.StatusOK
}
- if resp != nil && !ok {
- if b, err = xml.Marshal(resp); err != nil {
- return err
+ // if no data payload is provided, send the response status
+ if response.Data == nil {
+ ctx.Status(opts.Status)
+ return nil
+ }
+
+ var responseBytes []byte
+
+ // Handle already encoded responses(text, json...)
+ encodedResp, ok := response.Data.([]byte)
+ if ok {
+ responseBytes = encodedResp
+ } else {
+ if responseBytes, err = xml.Marshal(response.Data); err != nil {
+ debuglogger.Logf("Internal Error, %v", err)
+ return ctx.Status(http.StatusInternalServerError).Send(s3err.GetAPIErrorResponse(
+ s3err.GetAPIError(s3err.ErrInternalError), "", "", ""))
}
- if len(b) > 0 {
- ctx.Response().Header.Set("Content-Length", fmt.Sprint(len(b)))
+ if len(responseBytes) > 0 {
ctx.Response().Header.SetContentType(fiber.MIMEApplicationXML)
}
}
- if l.Logger != nil {
- l.Logger.Log(ctx, nil, b, s3log.LogMeta{
- Action: l.Action,
- BucketOwner: l.BucketOwner,
- ObjectSize: l.ObjectSize,
+ if svc.Logger != nil {
+ svc.Logger.Log(ctx, nil, responseBytes, s3log.LogMeta{
+ Action: s3action,
+ BucketOwner: opts.BucketOwner,
+ ObjectSize: opts.ObjectSize,
})
}
- if l.EvSender != nil {
- l.EvSender.SendEvent(ctx, s3event.EventMeta{
- BucketOwner: l.BucketOwner,
- ObjectSize: l.ObjectSize,
- ObjectETag: l.ObjectETag,
- VersionId: l.VersionId,
- EventName: l.EventName,
+ if svc.EventSender != nil {
+ svc.EventSender.SendEvent(ctx, s3event.EventMeta{
+ BucketOwner: opts.BucketOwner,
+ ObjectSize: opts.ObjectSize,
+ ObjectETag: opts.ObjectETag,
+ VersionId: opts.VersionId,
+ EventName: opts.EventName,
})
}
if ok {
- if len(b) > 0 {
- ctx.Response().Header.Set("Content-Length", fmt.Sprint(len(b)))
+ if len(responseBytes) > 0 {
+ ctx.Response().Header.Set("Content-Length", fmt.Sprint(len(responseBytes)))
}
- return ctx.Send(b)
+ return ctx.Send(responseBytes)
}
- msglen := len(xmlhdr) + len(b)
+ msglen := len(xmlhdr) + len(responseBytes)
if msglen > maxXMLBodyLen {
debuglogger.Logf("XML encoded body len %v exceeds max len %v",
msglen, maxXMLBodyLen)
@@ -4042,7 +278,23 @@ func SendXMLResponse(ctx *fiber.Ctx, resp any, err error, l *MetaOpts) error {
}
res := make([]byte, 0, msglen)
res = append(res, xmlhdr...)
- res = append(res, b...)
+ res = append(res, responseBytes...)
+
+ // Set the Content-Length header
+ ctx.Response().Header.SetContentLength(msglen)
return ctx.Send(res)
}
+
+// Sets the response headers
+func SetResponseHeaders(ctx *fiber.Ctx, headers map[string]*string) {
+ if headers == nil {
+ return
+ }
+ for key, val := range headers {
+ if val == nil || *val == "" {
+ continue
+ }
+ ctx.Response().Header.Add(key, *val)
+ }
+}
diff --git a/s3api/controllers/base_test.go b/s3api/controllers/base_test.go
index f9d83d54..186726a5 100644
--- a/s3api/controllers/base_test.go
+++ b/s3api/controllers/base_test.go
@@ -15,2029 +15,713 @@
package controllers
import (
- "bufio"
- "context"
- "encoding/json"
+ "bytes"
+ "encoding/xml"
+ "errors"
"fmt"
"net/http"
"net/http/httptest"
- "reflect"
+ "net/url"
+ "path"
"strings"
"testing"
- "time"
- "github.com/aws/aws-sdk-go-v2/service/s3"
- "github.com/aws/aws-sdk-go-v2/service/s3/types"
"github.com/gofiber/fiber/v2"
+ "github.com/stretchr/testify/assert"
"github.com/valyala/fasthttp"
"github.com/versity/versitygw/auth"
"github.com/versity/versitygw/backend"
+ "github.com/versity/versitygw/metrics"
"github.com/versity/versitygw/s3api/utils"
"github.com/versity/versitygw/s3err"
+ "github.com/versity/versitygw/s3event"
+ "github.com/versity/versitygw/s3log"
"github.com/versity/versitygw/s3response"
)
var (
- acl auth.ACL
- acldata []byte
+ defaultLocals map[utils.ContextKey]any = map[utils.ContextKey]any{
+ utils.ContextKeyIsRoot: true,
+ utils.ContextKeyParsedAcl: auth.ACL{
+ Owner: "root",
+ },
+ utils.ContextKeyAccount: auth.Account{
+ Access: "root",
+ Role: auth.RoleAdmin,
+ },
+ }
+
+ accessDeniedLocals map[utils.ContextKey]any = map[utils.ContextKey]any{
+ utils.ContextKeyIsRoot: false,
+ utils.ContextKeyParsedAcl: auth.ACL{
+ Owner: "root",
+ },
+ utils.ContextKeyAccount: auth.Account{
+ Access: "user",
+ Role: auth.RoleUser,
+ },
+ }
)
-func init() {
- var err error
- acldata, err = json.Marshal(acl)
- if err != nil {
- panic(err)
+type testInput struct {
+ bucket string
+ body []byte
+ locals map[utils.ContextKey]any
+ headers map[string]string
+ queries map[string]string
+ beRes any
+ beErr error
+ extraMockErr error
+ extraMockResp any
+}
+
+type testOutput struct {
+ response *Response
+ err error
+}
+
+type ctxInputs struct {
+ bucket string
+ object string
+ body []byte
+ locals map[utils.ContextKey]any
+ headers map[string]string
+ queries map[string]string
+}
+
+func testController(t *testing.T, ctrl Controller, resp *Response, expectedErr error, input ctxInputs) {
+ app := fiber.New()
+
+ app.Post("/:bucket/*", func(ctx *fiber.Ctx) error {
+ // set the request body
+ ctx.Request().SetBody(input.body)
+ // set the request locals
+ if input.locals != nil {
+ for key, local := range input.locals {
+ key.Set(ctx, local)
+ }
+ }
+
+ // call the controller by passing the ctx
+ res, err := ctrl(ctx)
+ assert.Equal(t, resp, res)
+ if expectedErr != nil {
+ assert.Error(t, err)
+
+ switch expectedErr.(type) {
+ case s3err.APIError:
+ assert.EqualValues(t, expectedErr, err)
+ default:
+ assert.ErrorContains(t, err, expectedErr.Error())
+ }
+ } else {
+ assert.NoError(t, err)
+ }
+
+ return nil
+ })
+
+ req := buildRequest(input.bucket, input.object, input.body, input.headers, input.queries)
+
+ _, err := app.Test(req)
+ assert.NoError(t, err)
+}
+
+func buildRequest(bucket, object string, body []byte, headers, queries map[string]string) *http.Request {
+ if bucket == "" {
+ bucket = "bucket"
}
+ if object == "" {
+ object = "object"
+ }
+ uri := url.URL{
+ Path: "/" + path.Join(bucket, object),
+ }
+
+ // set the request query params
+ if queries != nil {
+ q := uri.Query()
+ for key, val := range queries {
+ q.Set(key, val)
+ }
+
+ uri.RawQuery = q.Encode()
+ }
+
+ // create a new request
+ req := httptest.NewRequest(http.MethodPost, uri.String(), bytes.NewReader(body))
+
+ // set the request headers
+ for key, val := range headers {
+ req.Header.Set(key, val)
+ }
+
+ return req
}
func TestNew(t *testing.T) {
type args struct {
- be backend.Backend
- iam auth.IAMService
+ be backend.Backend
+ iam auth.IAMService
+ logger s3log.AuditLogger
+ evs s3event.S3EventSender
+ mm metrics.Manager
+ debug bool
+ readonly bool
}
-
- be := backend.BackendUnsupported{}
-
tests := []struct {
name string
args args
want S3ApiController
}{
{
- name: "Initialize S3 api controller",
+ name: "debug enabled",
args: args{
- be: be,
- iam: &auth.IAMServiceInternal{},
+ debug: true,
},
want: S3ApiController{
- be: be,
- iam: &auth.IAMServiceInternal{},
+ debug: true,
+ },
+ },
+ {
+ name: "debug disabled",
+ args: args{
+ debug: false,
+ },
+ want: S3ApiController{
+ debug: false,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
- got := New(tt.args.be, tt.args.iam, nil, nil, nil, false, false)
- if !reflect.DeepEqual(got, tt.want) {
- t.Errorf("New() = %v, want %v", got, tt.want)
- }
+ got := New(tt.args.be, tt.args.iam, tt.args.logger, tt.args.evs, tt.args.mm, tt.args.debug, tt.args.readonly)
+ assert.Equal(t, got, tt.want)
})
}
}
-func TestS3ApiController_ListBuckets(t *testing.T) {
- type args struct {
- req *http.Request
- }
-
- app := fiber.New()
- s3ApiController := S3ApiController{
- be: &BackendMock{
- ListBucketsFunc: func(contextMoqParam context.Context, listBucketsInput s3response.ListBucketsInput) (s3response.ListAllMyBucketsResult, error) {
- return s3response.ListAllMyBucketsResult{}, nil
- },
- },
- }
-
- app.Use(func(ctx *fiber.Ctx) error {
- utils.ContextKeyAccount.Set(ctx, auth.Account{Access: "valid access", Role: "admin:"})
- return ctx.Next()
- })
- app.Get("/", s3ApiController.ListBuckets)
-
- // Error case
- appErr := fiber.New()
- s3ApiControllerErr := S3ApiController{
- be: &BackendMock{
- ListBucketsFunc: func(contextMoqParam context.Context, listBucketsInput s3response.ListBucketsInput) (s3response.ListAllMyBucketsResult, error) {
- return s3response.ListAllMyBucketsResult{}, s3err.GetAPIError(s3err.ErrMethodNotAllowed)
- },
- },
- }
-
- appErr.Use(func(ctx *fiber.Ctx) error {
- utils.ContextKeyAccount.Set(ctx, auth.Account{Access: "valid access", Role: "admin:"})
- return ctx.Next()
- })
- appErr.Get("/", s3ApiControllerErr.ListBuckets)
-
+func TestS3ApiController_HandleErrorRoute(t *testing.T) {
tests := []struct {
- name string
- args args
- app *fiber.App
- wantErr bool
- statusCode int
+ name string
+ input testInput
+ output testOutput
}{
{
- name: "List-bucket-method-not-allowed",
- args: args{
- req: httptest.NewRequest(http.MethodGet, "/", nil),
+ name: "should return the passed error",
+ input: testInput{
+ extraMockErr: s3err.GetAPIError(s3err.ErrAnonymousCreateMp),
},
- app: appErr,
- wantErr: false,
- statusCode: 405,
- },
- {
- name: "list-bucket-success",
- args: args{
- req: httptest.NewRequest(http.MethodGet, "/", nil),
+ output: testOutput{
+ response: &Response{},
+ err: s3err.GetAPIError(s3err.ErrAnonymousCreateMp),
},
- app: app,
- wantErr: false,
- statusCode: 200,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
- resp, err := tt.app.Test(tt.args.req)
-
- if (err != nil) != tt.wantErr {
- t.Errorf("S3ApiController.ListBuckets() error = %v, wantErr %v", err, tt.wantErr)
- }
-
- if resp.StatusCode != tt.statusCode {
- t.Errorf("S3ApiController.ListBuckets() statusCode = %v, wantStatusCode = %v", resp.StatusCode, tt.statusCode)
- }
+ s3Ctrl := S3ApiController{}
+ ctrl := s3Ctrl.HandleErrorRoute(tt.input.extraMockErr)
+ testController(
+ t,
+ ctrl,
+ tt.output.response,
+ tt.output.err,
+ ctxInputs{})
})
}
}
-func getPtr(val string) *string {
- return &val
-}
-
-func TestS3ApiController_GetActions(t *testing.T) {
+func TestSetResponseHeaders(t *testing.T) {
type args struct {
- req *http.Request
+ headers map[string]*string
}
-
- now := time.Now()
-
- app := fiber.New()
- contentLength := int64(1000)
- s3ApiController := S3ApiController{
- be: &BackendMock{
- GetBucketAclFunc: func(context.Context, *s3.GetBucketAclInput) ([]byte, error) {
- return acldata, nil
- },
- ListPartsFunc: func(context.Context, *s3.ListPartsInput) (s3response.ListPartsResult, error) {
- return s3response.ListPartsResult{}, nil
- },
- GetObjectAclFunc: func(context.Context, *s3.GetObjectAclInput) (*s3.GetObjectAclOutput, error) {
- return &s3.GetObjectAclOutput{}, nil
- },
- GetObjectAttributesFunc: func(context.Context, *s3.GetObjectAttributesInput) (s3response.GetObjectAttributesResponse, error) {
- return s3response.GetObjectAttributesResponse{}, nil
- },
- GetObjectFunc: func(context.Context, *s3.GetObjectInput) (*s3.GetObjectOutput, error) {
- return &s3.GetObjectOutput{
- Metadata: map[string]string{"hello": "world"},
- ContentType: getPtr("application/xml"),
- ContentEncoding: getPtr("gzip"),
- ETag: getPtr("98sda7f97sa9df798sd79f8as9df"),
- ContentLength: &contentLength,
- LastModified: &now,
- StorageClass: "storage class",
- }, nil
- },
- GetObjectTaggingFunc: func(_ context.Context, bucket, object string) (map[string]string, error) {
- return map[string]string{"hello": "world"}, nil
- },
- GetObjectRetentionFunc: func(contextMoqParam context.Context, bucket, object, versionId string) ([]byte, error) {
- result, err := json.Marshal(types.ObjectLockRetention{
- Mode: types.ObjectLockRetentionModeCompliance,
- })
- if err != nil {
- return nil, err
- }
- return result, nil
- },
- GetObjectLegalHoldFunc: func(contextMoqParam context.Context, bucket, object, versionId string) (*bool, error) {
- result := true
- return &result, nil
- },
- },
- }
- app.Use(func(ctx *fiber.Ctx) error {
- utils.ContextKeyAccount.Set(ctx, auth.Account{Access: "valid access"})
- utils.ContextKeyIsRoot.Set(ctx, true)
- utils.ContextKeyParsedAcl.Set(ctx, auth.ACL{})
- return ctx.Next()
- })
- app.Get("/:bucket/:key/*", s3ApiController.GetActions)
-
- // GetObjectAttributes success case
- getObjAttrs := httptest.NewRequest(http.MethodGet, "/my-bucket/key", nil)
- getObjAttrs.Header.Set("X-Amz-Object-Attributes", "hello")
-
- invalidChecksumMode := httptest.NewRequest(http.MethodGet, "/my-bucket/key", nil)
- invalidChecksumMode.Header.Set("x-amz-checksum-mode", "invalid_checksum_mode")
-
tests := []struct {
- name string
- app *fiber.App
- args args
- wantErr bool
- statusCode int
+ name string
+ args args
+ expected map[string]string
}{
{
- name: "Get-actions-get-tags-success",
- app: app,
+ name: "should not set if map is nil",
args: args{
- req: httptest.NewRequest(http.MethodGet, "/my-bucket/key/key.json?tagging", nil),
+ headers: nil,
},
- wantErr: false,
- statusCode: 200,
+ expected: nil,
},
{
- name: "Get-actions-get-object-retention-success",
- app: app,
+ name: "should set some headers",
args: args{
- req: httptest.NewRequest(http.MethodGet, "/my-bucket/my-obj?retention", nil),
- },
- wantErr: false,
- statusCode: 200,
- },
- {
- name: "Get-actions-get-object-legal-hold-success",
- app: app,
- args: args{
- req: httptest.NewRequest(http.MethodGet, "/my-bucket/my-obj?legal-hold", nil),
- },
- wantErr: false,
- statusCode: 200,
- },
- {
- name: "Get-actions-invalid-max-parts-string",
- app: app,
- args: args{
- req: httptest.NewRequest(http.MethodGet, "/my-bucket/key?uploadId=hello&max-parts=invalid", nil),
- },
- wantErr: false,
- statusCode: 400,
- },
- {
- name: "Get-actions-invalid-max-parts-negative",
- app: app,
- args: args{
- req: httptest.NewRequest(http.MethodGet, "/my-bucket/key?uploadId=hello&max-parts=-8", nil),
- },
- wantErr: false,
- statusCode: 400,
- },
- {
- name: "Get-actions-invalid-part-number-marker-string",
- app: app,
- args: args{
- req: httptest.NewRequest(http.MethodGet, "/my-bucket/key?uploadId=hello&max-parts=200&part-number-marker=invalid", nil),
- },
- wantErr: false,
- statusCode: 400,
- },
- {
- name: "Get-actions-invalid-part-number-marker-negative",
- app: app,
- args: args{
- req: httptest.NewRequest(http.MethodGet, "/my-bucket/key?uploadId=hello&max-parts=200&part-number-marker=-8", nil),
- },
- wantErr: false,
- statusCode: 400,
- },
- {
- name: "Get-actions-list-object-parts-success",
- app: app,
- args: args{
- req: httptest.NewRequest(http.MethodGet, "/my-bucket/key?uploadId=hello&max-parts=200&part-number-marker=23", nil),
- },
- wantErr: false,
- statusCode: 200,
- },
- {
- name: "Get-actions-get-object-acl-success",
- app: app,
- args: args{
- req: httptest.NewRequest(http.MethodGet, "/my-bucket/key?acl", nil),
- },
- wantErr: false,
- statusCode: 200,
- },
- {
- name: "Get-actions-get-object-attributes-success",
- app: app,
- args: args{
- req: getObjAttrs,
- },
- wantErr: false,
- statusCode: 200,
- },
- {
- name: "Get-actions-get-object-invalid-checksum-mode",
- app: app,
- args: args{
- req: invalidChecksumMode,
- },
- wantErr: false,
- statusCode: 400,
- },
- {
- name: "Get-actions-get-object-success",
- app: app,
- args: args{
- req: httptest.NewRequest(http.MethodGet, "/my-bucket/key", nil),
- },
- wantErr: false,
- statusCode: 200,
- },
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- resp, err := tt.app.Test(tt.args.req)
-
- if (err != nil) != tt.wantErr {
- t.Errorf("S3ApiController.GetActions() error = %v, wantErr %v", err, tt.wantErr)
- }
-
- if resp.StatusCode != tt.statusCode {
- t.Errorf("S3ApiController.GetActions() statusCode = %v, wantStatusCode = %v", resp.StatusCode, tt.statusCode)
- }
- })
- }
-}
-
-func TestS3ApiController_ListActions(t *testing.T) {
- type args struct {
- req *http.Request
- }
-
- objectLockResult, err := json.Marshal(auth.BucketLockConfig{})
- if err != nil {
- t.Errorf("failed to parse object lock result %v", err)
- }
-
- app := fiber.New()
- s3ApiController := S3ApiController{
- be: &BackendMock{
- GetBucketAclFunc: func(context.Context, *s3.GetBucketAclInput) ([]byte, error) {
- return acldata, nil
- },
- ListMultipartUploadsFunc: func(_ context.Context, output *s3.ListMultipartUploadsInput) (s3response.ListMultipartUploadsResult, error) {
- return s3response.ListMultipartUploadsResult{}, nil
- },
- ListObjectsV2Func: func(context.Context, *s3.ListObjectsV2Input) (s3response.ListObjectsV2Result, error) {
- return s3response.ListObjectsV2Result{}, nil
- },
- ListObjectsFunc: func(context.Context, *s3.ListObjectsInput) (s3response.ListObjectsResult, error) {
- return s3response.ListObjectsResult{}, nil
- },
- GetBucketTaggingFunc: func(contextMoqParam context.Context, bucket string) (map[string]string, error) {
- return map[string]string{}, nil
- },
- GetBucketVersioningFunc: func(contextMoqParam context.Context, bucket string) (s3response.GetBucketVersioningOutput, error) {
- return s3response.GetBucketVersioningOutput{}, nil
- },
- ListObjectVersionsFunc: func(contextMoqParam context.Context, listObjectVersionsInput *s3.ListObjectVersionsInput) (s3response.ListVersionsResult, error) {
- return s3response.ListVersionsResult{}, nil
- },
- GetBucketPolicyFunc: func(contextMoqParam context.Context, bucket string) ([]byte, error) {
- return []byte{}, nil
- },
- GetObjectLockConfigurationFunc: func(contextMoqParam context.Context, bucket string) ([]byte, error) {
- return objectLockResult, nil
- },
- GetBucketOwnershipControlsFunc: func(contextMoqParam context.Context, bucket string) (types.ObjectOwnership, error) {
- return types.ObjectOwnershipBucketOwnerEnforced, nil
- },
- },
- }
-
- app.Use(func(ctx *fiber.Ctx) error {
- utils.ContextKeyAccount.Set(ctx, auth.Account{Access: "valid access"})
- utils.ContextKeyIsRoot.Set(ctx, true)
- utils.ContextKeyParsedAcl.Set(ctx, auth.ACL{})
- return ctx.Next()
- })
-
- app.Get("/:bucket", s3ApiController.ListActions)
-
- // Error case
- s3ApiControllerError := S3ApiController{
- be: &BackendMock{
- GetBucketAclFunc: func(context.Context, *s3.GetBucketAclInput) ([]byte, error) {
- return acldata, nil
- },
- ListObjectsFunc: func(context.Context, *s3.ListObjectsInput) (s3response.ListObjectsResult, error) {
- return s3response.ListObjectsResult{}, s3err.GetAPIError(s3err.ErrNotImplemented)
- },
- GetBucketTaggingFunc: func(contextMoqParam context.Context, bucket string) (map[string]string, error) {
- return nil, s3err.GetAPIError(s3err.ErrNoSuchBucket)
- },
- },
- }
- appError := fiber.New()
- appError.Use(func(ctx *fiber.Ctx) error {
- utils.ContextKeyAccount.Set(ctx, auth.Account{Access: "valid access"})
- utils.ContextKeyIsRoot.Set(ctx, true)
- utils.ContextKeyParsedAcl.Set(ctx, auth.ACL{})
- return ctx.Next()
- })
- appError.Get("/:bucket", s3ApiControllerError.ListActions)
-
- tests := []struct {
- name string
- app *fiber.App
- args args
- wantErr bool
- statusCode int
- }{
- {
- name: "Get-bucket-tagging-non-existing-bucket",
- app: appError,
- args: args{
- req: httptest.NewRequest(http.MethodGet, "/my-bucket?tagging", nil),
- },
- wantErr: false,
- statusCode: 404,
- },
- {
- name: "Get-bucket-ownership-control-success",
- app: app,
- args: args{
- req: httptest.NewRequest(http.MethodGet, "/my-bucket?ownershipControls", nil),
- },
- wantErr: false,
- statusCode: 200,
- },
- {
- name: "Get-bucket-tagging-success",
- app: app,
- args: args{
- req: httptest.NewRequest(http.MethodGet, "/my-bucket?tagging", nil),
- },
- wantErr: false,
- statusCode: 200,
- },
- {
- name: "Get-object-lock-configuration-success",
- app: app,
- args: args{
- req: httptest.NewRequest(http.MethodGet, "/my-bucket?object-lock", nil),
- },
- wantErr: false,
- statusCode: 200,
- },
- {
- name: "Get-bucket-acl-success",
- app: app,
- args: args{
- req: httptest.NewRequest(http.MethodGet, "/my-bucket?acl=acl", nil),
- },
- wantErr: false,
- statusCode: 200,
- },
- {
- name: "List-Multipart-Upload-success",
- app: app,
- args: args{
- req: httptest.NewRequest(http.MethodGet, "/my-bucket?uploads=uploads", nil),
- },
- wantErr: false,
- statusCode: 200,
- },
- {
- name: "List-Objects-V2-success",
- app: app,
- args: args{
- req: httptest.NewRequest(http.MethodGet, "/my-bucket?list-type=2", nil),
- },
- wantErr: false,
- statusCode: 200,
- },
- {
- name: "List-Objects-V1-success",
- app: app,
- args: args{
- req: httptest.NewRequest(http.MethodGet, "/my-bucket", nil),
- },
- wantErr: false,
- statusCode: 200,
- },
- {
- name: "List-actions-error-case",
- app: appError,
- args: args{
- req: httptest.NewRequest(http.MethodGet, "/my-bucket", nil),
- },
- wantErr: false,
- statusCode: 501,
- },
- {
- name: "List-actions-get-bucket-versioning-success",
- app: app,
- args: args{
- req: httptest.NewRequest(http.MethodGet, "/my-bucket?versioning", nil),
- },
- wantErr: false,
- statusCode: 200,
- },
- {
- name: "List-actions-get-bucket-policy-success",
- app: app,
- args: args{
- req: httptest.NewRequest(http.MethodGet, "/my-bucket?policy", nil),
- },
- wantErr: false,
- statusCode: 200,
- },
- {
- name: "List-actions-list-object-versions-success",
- app: app,
- args: args{
- req: httptest.NewRequest(http.MethodGet, "/my-bucket?versions", nil),
- },
- wantErr: false,
- statusCode: 200,
- },
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- resp, err := tt.app.Test(tt.args.req)
-
- if (err != nil) != tt.wantErr {
- t.Errorf("S3ApiController.ListActions() error = %v, wantErr %v", err, tt.wantErr)
- }
-
- if resp.StatusCode != tt.statusCode {
- t.Errorf("S3ApiController.ListActions() statusCode = %v, wantStatusCode = %v", resp.StatusCode, tt.statusCode)
- }
- })
- }
-}
-
-func TestS3ApiController_PutBucketActions(t *testing.T) {
- type args struct {
- req *http.Request
- }
-
- app := fiber.New()
-
- // Mock valid acl
- acl := auth.ACL{Owner: "valid access"}
- acldata, err := json.Marshal(acl)
- if err != nil {
- t.Errorf("Failed to parse the params: %v", err.Error())
- return
- }
-
- body := `
-
-
-
-
- hell
-
- string
-
-
-
- hello
-
-
- `
-
- invOwnerBody := `
-
-
- hello
-
-
- `
-
- tagBody := `
-
-
-
- organization
- marketing
-
-
-
- `
-
- versioningBody := `
-
- Enabled
- Enabled
-
- `
-
- policyBody := `{
- "Statement": [
- {
- "Effect": "Allow",
- "Principal": "*",
- "Action": "s3:GetObject",
- "Resource": "arn:aws:s3:::my-bucket/*"
- }
- ]
- }
- `
-
- objectLockBody := `
-
- Enabled
-
-
- GOVERNANCE
- 2
-
-
-
- `
-
- ownershipBody := `
-
-
- BucketOwnerEnforced
-
-
- `
-
- invalidOwnershipBody := `
-
-
- invalid_value
-
-
- `
-
- s3ApiController := S3ApiController{
- be: &BackendMock{
- GetBucketAclFunc: func(context.Context, *s3.GetBucketAclInput) ([]byte, error) {
- return acldata, nil
- },
- PutBucketAclFunc: func(context.Context, string, []byte) error {
- return nil
- },
- CreateBucketFunc: func(context.Context, *s3.CreateBucketInput, []byte) error {
- return nil
- },
- PutBucketTaggingFunc: func(contextMoqParam context.Context, bucket string, tags map[string]string) error {
- return nil
- },
- PutBucketVersioningFunc: func(contextMoqParam context.Context, bucket string, status types.BucketVersioningStatus) error {
- return nil
- },
- PutBucketPolicyFunc: func(contextMoqParam context.Context, bucket string, policy []byte) error {
- return nil
- },
- PutObjectLockConfigurationFunc: func(contextMoqParam context.Context, bucket string, config []byte) error {
- return nil
- },
- PutBucketOwnershipControlsFunc: func(contextMoqParam context.Context, bucket string, ownership types.ObjectOwnership) error {
- return nil
- },
- GetBucketOwnershipControlsFunc: func(contextMoqParam context.Context, bucket string) (types.ObjectOwnership, error) {
- return types.ObjectOwnershipBucketOwnerPreferred, nil
- },
- },
- }
- // Mock ctx.Locals
- app.Use(func(ctx *fiber.Ctx) error {
- utils.ContextKeyAccount.Set(ctx, auth.Account{Access: "valid access"})
- utils.ContextKeyIsRoot.Set(ctx, true)
- utils.ContextKeyParsedAcl.Set(ctx, auth.ACL{Owner: "valid access"})
- return ctx.Next()
- })
- app.Put("/:bucket", s3ApiController.PutBucketActions)
-
- // invalid acl case
- invAclReq := httptest.NewRequest(http.MethodPut, "/my-bucket?acl", nil)
- invAclReq.Header.Set("X-Amz-Acl", "invalid")
-
- // invalid acl case 2
- errAclReq := httptest.NewRequest(http.MethodPut, "/my-bucket?acl", nil)
- errAclReq.Header.Set("X-Amz-Acl", "private")
- errAclReq.Header.Set("X-Amz-Grant-Read", "hello")
-
- // PutBucketAcl incorrect bucket owner case
- incorrectBucketOwner := httptest.NewRequest(http.MethodPut, "/my-bucket?acl", strings.NewReader(invOwnerBody))
-
- // PutBucketAcl acl success
- aclSuccReq := httptest.NewRequest(http.MethodPut, "/my-bucket?acl", nil)
- aclSuccReq.Header.Set("X-Amz-Acl", "private")
-
- // Invalid acl body case
- errAclBodyReq := httptest.NewRequest(http.MethodPut, "/my-bucket?acl", strings.NewReader(body))
- errAclBodyReq.Header.Set("X-Amz-Grant-Read", "hello")
-
- invAclOwnershipReq := httptest.NewRequest(http.MethodPut, "/my-bucket", nil)
- invAclOwnershipReq.Header.Set("X-Amz-Grant-Read", "hello")
-
- tests := []struct {
- name string
- app *fiber.App
- args args
- wantErr bool
- statusCode int
- }{
- {
- name: "Put-bucket-tagging-invalid-body",
- app: app,
- args: args{
- req: httptest.NewRequest(http.MethodPut, "/my-bucket?tagging", nil),
- },
- wantErr: false,
- statusCode: 400,
- },
- {
- name: "Put-bucket-tagging-success",
- app: app,
- args: args{
- req: httptest.NewRequest(http.MethodPut, "/my-bucket?tagging", strings.NewReader(tagBody)),
- },
- wantErr: false,
- statusCode: 204,
- },
- {
- name: "Put-bucket-ownership-controls-invalid-ownership",
- app: app,
- args: args{
- req: httptest.NewRequest(http.MethodPut, "/my-bucket?ownershipControls", strings.NewReader(invalidOwnershipBody)),
- },
- wantErr: false,
- statusCode: 400,
- },
- {
- name: "Put-bucket-ownership-controls-success",
- app: app,
- args: args{
- req: httptest.NewRequest(http.MethodPut, "/my-bucket?ownershipControls", strings.NewReader(ownershipBody)),
- },
- wantErr: false,
- statusCode: 200,
- },
- {
- name: "Put-object-lock-configuration-invalid-body",
- app: app,
- args: args{
- req: httptest.NewRequest(http.MethodPut, "/my-bucket?object-lock", nil),
- },
- wantErr: false,
- statusCode: 400,
- },
- {
- name: "Put-object-lock-configuration-success",
- app: app,
- args: args{
- req: httptest.NewRequest(http.MethodPut, "/my-bucket?object-lock", strings.NewReader(objectLockBody)),
- },
- wantErr: false,
- statusCode: 200,
- },
- {
- name: "Put-bucket-versioning-invalid-body",
- app: app,
- args: args{
- req: httptest.NewRequest(http.MethodPut, "/my-bucket?versioning", nil),
- },
- wantErr: false,
- statusCode: 400,
- },
- {
- name: "Put-bucket-versioning-success",
- app: app,
- args: args{
- req: httptest.NewRequest(http.MethodPut, "/my-bucket?versioning", strings.NewReader(versioningBody)),
- },
- wantErr: false,
- statusCode: 200,
- },
- {
- name: "Put-bucket-policy-invalid-body",
- app: app,
- args: args{
- req: httptest.NewRequest(http.MethodPut, "/my-bucket?policy", nil),
- },
- wantErr: false,
- statusCode: 400,
- },
- {
- name: "Put-bucket-policy-success",
- app: app,
- args: args{
- req: httptest.NewRequest(http.MethodPut, "/my-bucket?policy", strings.NewReader(policyBody)),
- },
- wantErr: false,
- statusCode: 200,
- },
- {
- name: "Put-bucket-acl-invalid-acl",
- app: app,
- args: args{
- req: invAclReq,
- },
- wantErr: false,
- statusCode: 400,
- },
- {
- name: "Put-bucket-acl-incorrect-acl",
- app: app,
- args: args{
- req: errAclReq,
- },
- wantErr: false,
- statusCode: 400,
- },
- {
- name: "Put-bucket-acl-incorrect-acl-body",
- app: app,
- args: args{
- req: errAclBodyReq,
- },
- wantErr: false,
- statusCode: 400,
- },
- {
- name: "Put-bucket-acl-incorrect-bucket-owner",
- app: app,
- args: args{
- req: incorrectBucketOwner,
- },
- wantErr: false,
- statusCode: 400,
- },
- {
- name: "Put-bucket-acl-success",
- app: app,
- args: args{
- req: aclSuccReq,
- },
- wantErr: false,
- statusCode: 200,
- },
- {
- name: "Create-bucket-invalid-acl-ownership-combination",
- app: app,
- args: args{
- req: invAclOwnershipReq,
- },
- wantErr: false,
- statusCode: 400,
- },
- {
- name: "Create-bucket-success",
- app: app,
- args: args{
- req: httptest.NewRequest(http.MethodPut, "/my-bucket", nil),
- },
- wantErr: false,
- statusCode: 200,
- },
- }
- for _, tt := range tests {
- resp, err := tt.app.Test(tt.args.req)
-
- if (err != nil) != tt.wantErr {
- t.Errorf("S3ApiController.PutBucketActions() error = %v, wantErr %v", err, tt.wantErr)
- }
-
- if resp.StatusCode != tt.statusCode {
- t.Errorf("S3ApiController.PutBucketActions() statusCode = %v, wantStatusCode = %v", resp.StatusCode, tt.statusCode)
- }
- }
-}
-
-func TestS3ApiController_PutActions(t *testing.T) {
- type args struct {
- req *http.Request
- }
-
- body := `
-
-
-
-
- hell
-
- string
-
-
-
- hello
-
-
- `
- tagBody := `
-
-
-
- string
- string
-
-
-
- `
-
- //retentionBody := `
- //
- // GOVERNANCE
- // 2025-01-01T00:00:00Z
- //
- //`
-
- legalHoldBody := `
-
- ON
-
- `
-
- app := fiber.New()
- s3ApiController := S3ApiController{
- be: &BackendMock{
- GetBucketAclFunc: func(context.Context, *s3.GetBucketAclInput) ([]byte, error) {
- return acldata, nil
- },
- PutObjectAclFunc: func(context.Context, *s3.PutObjectAclInput) error {
- return nil
- },
- CopyObjectFunc: func(context.Context, s3response.CopyObjectInput) (s3response.CopyObjectOutput, error) {
- return s3response.CopyObjectOutput{
- CopyObjectResult: &s3response.CopyObjectResult{},
- }, nil
- },
- PutObjectFunc: func(context.Context, s3response.PutObjectInput) (s3response.PutObjectOutput, error) {
- return s3response.PutObjectOutput{}, nil
- },
- UploadPartFunc: func(context.Context, *s3.UploadPartInput) (*s3.UploadPartOutput, error) {
- return &s3.UploadPartOutput{}, nil
- },
- PutObjectTaggingFunc: func(_ context.Context, bucket, object string, tags map[string]string) error {
- return nil
- },
- UploadPartCopyFunc: func(context.Context, *s3.UploadPartCopyInput) (s3response.CopyPartResult, error) {
- return s3response.CopyPartResult{}, nil
- },
- PutObjectLegalHoldFunc: func(contextMoqParam context.Context, bucket, object, versionId string, status bool) error {
- return nil
- },
- PutObjectRetentionFunc: func(contextMoqParam context.Context, bucket, object, versionId string, bypass bool, retention []byte) error {
- return nil
- },
- GetObjectLockConfigurationFunc: func(contextMoqParam context.Context, bucket string) ([]byte, error) {
- return nil, s3err.GetAPIError(s3err.ErrObjectLockConfigurationNotFound)
- },
- },
- }
- app.Use(func(ctx *fiber.Ctx) error {
- utils.ContextKeyAccount.Set(ctx, auth.Account{Access: "valid access"})
- utils.ContextKeyIsRoot.Set(ctx, true)
- utils.ContextKeyParsedAcl.Set(ctx, auth.ACL{})
- return ctx.Next()
- })
- app.Put("/:bucket/:key/*", s3ApiController.PutActions)
-
- // UploadPartCopy success
- uploadPartCpyReq := httptest.NewRequest(http.MethodPut, "/my-bucket/my-key?uploadId=12asd32&partNumber=3", nil)
- uploadPartCpyReq.Header.Set("X-Amz-Copy-Source", "srcBucket/srcObject")
-
- // UploadPartCopy error case
- uploadPartCpyErrReq := httptest.NewRequest(http.MethodPut, "/my-bucket/my-key?uploadId=12asd32&partNumber=invalid", nil)
- uploadPartCpyErrReq.Header.Set("X-Amz-Copy-Source", "srcBucket/srcObject")
-
- // CopyObject success
- cpySrcReq := httptest.NewRequest(http.MethodPut, "/my-bucket/my-key", nil)
- cpySrcReq.Header.Set("X-Amz-Copy-Source", "srcBucket/srcObject")
-
- // CopyObject invalid checksum algorithm
- cpyInvChecksumAlgo := httptest.NewRequest(http.MethodPut, "/my-bucket/my-key", nil)
- cpyInvChecksumAlgo.Header.Set("X-Amz-Copy-Source", "srcBucket/srcObject")
- cpyInvChecksumAlgo.Header.Set("X-Amz-Checksum-Algorithm", "invalid_checksum_algorithm")
-
- // PutObjectAcl success
- aclReq := httptest.NewRequest(http.MethodPut, "/my-bucket/my-key", nil)
- aclReq.Header.Set("X-Amz-Acl", "private")
-
- // PutObjectAcl success grt case
- aclGrtReq := httptest.NewRequest(http.MethodPut, "/my-bucket/my-key", nil)
- aclGrtReq.Header.Set("X-Amz-Grant-Read", "private")
-
- // invalid acl case 1
- invAclReq := httptest.NewRequest(http.MethodPut, "/my-bucket/my-key?acl", nil)
- invAclReq.Header.Set("X-Amz-Acl", "invalid")
-
- // invalid acl case 2
- errAclReq := httptest.NewRequest(http.MethodPut, "/my-bucket/my-key?acl", nil)
- errAclReq.Header.Set("X-Amz-Acl", "private")
- errAclReq.Header.Set("X-Amz-Grant-Read", "hello")
-
- // invalid body & grt case
- invAclBodyGrtReq := httptest.NewRequest(http.MethodPut, "/my-bucket/my-key?acl", strings.NewReader(body))
- invAclBodyGrtReq.Header.Set("X-Amz-Grant-Read", "hello")
-
- // PutObject invalid checksum algorithm
- invChecksumAlgo := httptest.NewRequest(http.MethodPut, "/my-bucket/my-key", nil)
- invChecksumAlgo.Header.Set("X-Amz-Checksum-Algorithm", "invalid_checksum_algorithm")
-
- // PutObject invalid base64 checksum
- invBase64Checksum := httptest.NewRequest(http.MethodPut, "/my-bucket/my-key", nil)
- invBase64Checksum.Header.Set("X-Amz-Checksum-Crc32", "invalid_base64")
-
- // PutObject invalid crc32
- invCrc32 := httptest.NewRequest(http.MethodPut, "/my-bucket/my-key", nil)
- invCrc32.Header.Set("X-Amz-Checksum-Crc32", "YXNkZmFkc2Zhc2Rm")
-
- // PutObject invalid crc32c
- invCrc32c := httptest.NewRequest(http.MethodPut, "/my-bucket/my-key", nil)
- invCrc32c.Header.Set("X-Amz-Checksum-Crc32c", "YXNkZmFkc2Zhc2RmYXNkZg==")
-
- // PutObject invalid sha1
- invSha1 := httptest.NewRequest(http.MethodPut, "/my-bucket/my-key", nil)
- invSha1.Header.Set("X-Amz-Checksum-Sha1", "YXNkZmFkc2Zhc2RmYXNkZnNkYWZkYXNmZGFzZg==")
-
- // PutObject invalid sha256
- invSha256 := httptest.NewRequest(http.MethodPut, "/my-bucket/my-key", nil)
- invSha256.Header.Set("X-Amz-Checksum-Sha256", "YXNkZmFkc2Zhc2RmYXNkZnNkYWZkYXNmZGFzZmFkc2Zhc2Rm")
-
- // PutObject multiple checksum headers
- mulChecksumHdrs := httptest.NewRequest(http.MethodPut, "/my-bucket/my-key", nil)
- mulChecksumHdrs.Header.Set("X-Amz-Checksum-Sha256", "d1SPCd/kZ2rAzbbLUC0n/bEaOSx70FNbXbIqoIxKuPY=")
- mulChecksumHdrs.Header.Set("X-Amz-Checksum-Crc32c", "ww2FVQ==")
-
- // PutObject checksum algorithm and header mismatch
- checksumHdrMismatch := httptest.NewRequest(http.MethodPut, "/my-bucket/my-key", nil)
- checksumHdrMismatch.Header.Set("X-Amz-Checksum-Algorithm", "SHA1")
- checksumHdrMismatch.Header.Set("X-Amz-Checksum-Crc32c", "ww2FVQ==")
-
- tests := []struct {
- name string
- app *fiber.App
- args args
- wantErr bool
- statusCode int
- }{
- {
- name: "Put-object-part-error-case",
- app: app,
- args: args{
- req: httptest.NewRequest(http.MethodPut, "/my-bucket/my-key?uploadId=abc&partNumber=invalid", nil),
- },
- wantErr: false,
- statusCode: 400,
- },
- {
- name: "Put-object-part-success",
- app: app,
- args: args{
- req: httptest.NewRequest(http.MethodPut, "/my-bucket/my-key?uploadId=4&partNumber=3", nil),
- },
- wantErr: false,
- statusCode: 200,
- },
- {
- name: "Set-tags-success",
- app: app,
- args: args{
- req: httptest.NewRequest(http.MethodPut, "/my-bucket/my-key?tagging", strings.NewReader(tagBody)),
- },
- wantErr: false,
- statusCode: 200,
- },
- {
- name: "put-object-retention-invalid-request",
- app: app,
- args: args{
- req: httptest.NewRequest(http.MethodPut, "/my-bucket/my-key?retention", nil),
- },
- wantErr: false,
- statusCode: 400,
- },
- //{
- // name: "put-object-retention-success",
- // app: app,
- // args: args{
- // req: httptest.NewRequest(http.MethodPut, "/my-bucket/my-key?retention", strings.NewReader(retentionBody)),
- // },
- // wantErr: false,
- // statusCode: 200,
- //},
- {
- name: "put-legal-hold-invalid-request",
- app: app,
- args: args{
- req: httptest.NewRequest(http.MethodPut, "/my-bucket/my-key?legal-hold", nil),
- },
- wantErr: false,
- statusCode: 400,
- },
- {
- name: "put-legal-hold-success",
- app: app,
- args: args{
- req: httptest.NewRequest(http.MethodPut, "/my-bucket/my-key?legal-hold", strings.NewReader(legalHoldBody)),
- },
- wantErr: false,
- statusCode: 200,
- },
- {
- name: "Put-object-acl-invalid-acl",
- app: app,
- args: args{
- req: invAclReq,
- },
- wantErr: false,
- statusCode: 400,
- },
- {
- name: "Put-object-acl-incorrect-acl",
- app: app,
- args: args{
- req: errAclReq,
- },
- wantErr: false,
- statusCode: 400,
- },
- {
- name: "Put-object-acl-incorrect-acl-body-case",
- app: app,
- args: args{
- req: invAclBodyGrtReq,
- },
- wantErr: false,
- statusCode: 400,
- },
- {
- name: "Put-object-acl-success",
- app: app,
- args: args{
- req: aclReq,
- },
- wantErr: false,
- statusCode: 200,
- },
- {
- name: "Put-object-acl-success-body-case",
- app: app,
- args: args{
- req: httptest.NewRequest(http.MethodPut, "/my-bucket/my-key?acl", strings.NewReader(body)),
- },
- wantErr: false,
- statusCode: 200,
- },
- {
- name: "Put-object-acl-success-grt-case",
- app: app,
- args: args{
- req: aclGrtReq,
- },
- wantErr: false,
- statusCode: 200,
- },
- {
- name: "Upload-part-copy-invalid-part-number",
- app: app,
- args: args{
- req: uploadPartCpyErrReq,
- },
- wantErr: false,
- statusCode: 400,
- },
- {
- name: "Upload-part-copy-success",
- app: app,
- args: args{
- req: uploadPartCpyReq,
- },
- wantErr: false,
- statusCode: 200,
- },
- {
- name: "Copy-object-invalid-checksum-algorithm",
- app: app,
- args: args{
- req: cpyInvChecksumAlgo,
- },
- wantErr: false,
- statusCode: 400,
- },
- {
- name: "Copy-object-success",
- app: app,
- args: args{
- req: cpySrcReq,
- },
- wantErr: false,
- statusCode: 200,
- },
- {
- name: "Put-object-success",
- app: app,
- args: args{
- req: httptest.NewRequest(http.MethodPut, "/my-bucket/my-key/key2", nil),
- },
- wantErr: false,
- statusCode: 200,
- },
- }
- for _, tt := range tests {
- resp, err := tt.app.Test(tt.args.req)
-
- if (err != nil) != tt.wantErr {
- t.Errorf("S3ApiController.PutActions() %v error = %v, wantErr %v",
- tt.name, err, tt.wantErr)
- }
-
- if resp.StatusCode != tt.statusCode {
- t.Errorf("S3ApiController.PutActions() %v statusCode = %v, wantStatusCode = %v",
- tt.name, resp.StatusCode, tt.statusCode)
- }
- }
-}
-
-func TestS3ApiController_DeleteBucket(t *testing.T) {
- type args struct {
- req *http.Request
- }
-
- app := fiber.New()
- s3ApiController := S3ApiController{
- be: &BackendMock{
- DeleteBucketFunc: func(_ context.Context, bucket string) error {
- return nil
- },
- DeleteBucketTaggingFunc: func(contextMoqParam context.Context, bucket string) error {
- return nil
- },
- DeleteBucketPolicyFunc: func(contextMoqParam context.Context, bucket string) error {
- return nil
- },
- DeleteBucketOwnershipControlsFunc: func(contextMoqParam context.Context, bucket string) error {
- return nil
- },
- },
- }
-
- app.Use(func(ctx *fiber.Ctx) error {
- utils.ContextKeyAccount.Set(ctx, auth.Account{Access: "valid access"})
- utils.ContextKeyIsRoot.Set(ctx, true)
- utils.ContextKeyParsedAcl.Set(ctx, auth.ACL{})
- return ctx.Next()
- })
-
- app.Delete("/:bucket", s3ApiController.DeleteBucket)
-
- tests := []struct {
- name string
- app *fiber.App
- args args
- wantErr bool
- statusCode int
- }{
- {
- name: "Delete-bucket-success",
- app: app,
- args: args{
- req: httptest.NewRequest(http.MethodDelete, "/my-bucket", nil),
- },
- wantErr: false,
- statusCode: 204,
- },
- {
- name: "Delete-bucket-tagging-success",
- app: app,
- args: args{
- req: httptest.NewRequest(http.MethodDelete, "/my-bucket?tagging", nil),
- },
- wantErr: false,
- statusCode: 204,
- },
- {
- name: "Delete-bucket-ownership-controls-success",
- app: app,
- args: args{
- req: httptest.NewRequest(http.MethodDelete, "/my-bucket?ownershipControls", nil),
- },
- wantErr: false,
- statusCode: 204,
- }, {
- name: "Delete-bucket-policy-success",
- app: app,
- args: args{
- req: httptest.NewRequest(http.MethodDelete, "/my-bucket?policy", nil),
- },
- wantErr: false,
- statusCode: 204,
- },
- }
- for _, tt := range tests {
- resp, err := tt.app.Test(tt.args.req)
-
- if (err != nil) != tt.wantErr {
- t.Errorf("S3ApiController.DeleteBucket() error = %v, wantErr %v", err, tt.wantErr)
- }
-
- if resp.StatusCode != tt.statusCode {
- t.Errorf("S3ApiController.DeleteBucket() statusCode = %v, wantStatusCode = %v", resp.StatusCode, tt.statusCode)
- }
- }
-}
-
-func TestS3ApiController_DeleteObjects(t *testing.T) {
- type args struct {
- req *http.Request
- }
-
- app := fiber.New()
- s3ApiController := S3ApiController{
- be: &BackendMock{
- GetBucketAclFunc: func(context.Context, *s3.GetBucketAclInput) ([]byte, error) {
- return acldata, nil
- },
- DeleteObjectsFunc: func(context.Context, *s3.DeleteObjectsInput) (s3response.DeleteResult, error) {
- return s3response.DeleteResult{}, nil
- },
- GetObjectLockConfigurationFunc: func(contextMoqParam context.Context, bucket string) ([]byte, error) {
- return nil, s3err.GetAPIError(s3err.ErrObjectLockConfigurationNotFound)
- },
- },
- }
-
- app.Use(func(ctx *fiber.Ctx) error {
- utils.ContextKeyAccount.Set(ctx, auth.Account{Access: "valid access"})
- utils.ContextKeyIsRoot.Set(ctx, true)
- utils.ContextKeyParsedAcl.Set(ctx, auth.ACL{})
- return ctx.Next()
- })
- app.Post("/:bucket", s3ApiController.DeleteObjects)
-
- // Valid request body
- xmlBody := `body`
-
- request := httptest.NewRequest(http.MethodPost, "/my-bucket", strings.NewReader(xmlBody))
- request.Header.Set("Content-Type", "application/xml")
-
- tests := []struct {
- name string
- app *fiber.App
- args args
- wantErr bool
- statusCode int
- }{
- {
- name: "Delete-Objects-success",
- app: app,
- args: args{
- req: request,
- },
- wantErr: false,
- statusCode: 200,
- },
- {
- name: "Delete-Objects-error",
- app: app,
- args: args{
- req: httptest.NewRequest(http.MethodPost, "/my-bucket", nil),
- },
- wantErr: false,
- statusCode: 400,
- },
- }
- for _, tt := range tests {
- resp, err := tt.app.Test(tt.args.req)
-
- if (err != nil) != tt.wantErr {
- t.Errorf("S3ApiController.DeleteObjects() error = %v, wantErr %v", err, tt.wantErr)
- }
-
- if resp.StatusCode != tt.statusCode {
- t.Errorf("S3ApiController.DeleteObjects() statusCode = %v, wantStatusCode = %v", resp.StatusCode, tt.statusCode)
- }
- }
-}
-
-func TestS3ApiController_DeleteActions(t *testing.T) {
- type args struct {
- req *http.Request
- }
-
- app := fiber.New()
- s3ApiController := S3ApiController{
- be: &BackendMock{
- GetBucketAclFunc: func(context.Context, *s3.GetBucketAclInput) ([]byte, error) {
- return acldata, nil
- },
- DeleteObjectFunc: func(contextMoqParam context.Context, deleteObjectInput *s3.DeleteObjectInput) (*s3.DeleteObjectOutput, error) {
- return &s3.DeleteObjectOutput{}, nil
- },
- AbortMultipartUploadFunc: func(context.Context, *s3.AbortMultipartUploadInput) error {
- return nil
- },
- DeleteObjectTaggingFunc: func(_ context.Context, bucket, object string) error {
- return nil
- },
- GetObjectLockConfigurationFunc: func(contextMoqParam context.Context, bucket string) ([]byte, error) {
- return nil, s3err.GetAPIError(s3err.ErrObjectLockConfigurationNotFound)
- },
- },
- }
-
- app.Use(func(ctx *fiber.Ctx) error {
- utils.ContextKeyAccount.Set(ctx, auth.Account{Access: "valid access"})
- utils.ContextKeyIsRoot.Set(ctx, true)
- utils.ContextKeyParsedAcl.Set(ctx, auth.ACL{})
- return ctx.Next()
- })
- app.Delete("/:bucket/:key/*", s3ApiController.DeleteActions)
-
- // Error case
- appErr := fiber.New()
-
- s3ApiControllerErr := S3ApiController{be: &BackendMock{
- GetBucketAclFunc: func(context.Context, *s3.GetBucketAclInput) ([]byte, error) {
- return acldata, nil
- },
- DeleteObjectFunc: func(contextMoqParam context.Context, deleteObjectInput *s3.DeleteObjectInput) (*s3.DeleteObjectOutput, error) {
- return nil, s3err.GetAPIError(s3err.ErrNoSuchKey)
- },
- GetObjectLockConfigurationFunc: func(contextMoqParam context.Context, bucket string) ([]byte, error) {
- return nil, s3err.GetAPIError(s3err.ErrObjectLockConfigurationNotFound)
- },
- }}
-
- appErr.Use(func(ctx *fiber.Ctx) error {
- utils.ContextKeyAccount.Set(ctx, auth.Account{Access: "valid access"})
- utils.ContextKeyIsRoot.Set(ctx, true)
- utils.ContextKeyParsedAcl.Set(ctx, auth.ACL{})
- return ctx.Next()
- })
- appErr.Delete("/:bucket/:key/*", s3ApiControllerErr.DeleteActions)
-
- tests := []struct {
- name string
- app *fiber.App
- args args
- wantErr bool
- statusCode int
- }{
- {
- name: "Abort-multipart-upload-success",
- app: app,
- args: args{
- req: httptest.NewRequest(http.MethodDelete, "/my-bucket/my-key?uploadId=324234", nil),
- },
- wantErr: false,
- statusCode: 204,
- },
- {
- name: "Remove-object-tagging-success",
- app: app,
- args: args{
- req: httptest.NewRequest(http.MethodDelete, "/my-bucket/my-key/key2?tagging", nil),
- },
- wantErr: false,
- statusCode: 204,
- },
- {
- name: "Delete-object-success",
- app: app,
- args: args{
- req: httptest.NewRequest(http.MethodDelete, "/my-bucket/my-key", nil),
- },
- wantErr: false,
- statusCode: 204,
- },
- {
- name: "Delete-object-error",
- app: appErr,
- args: args{
- req: httptest.NewRequest(http.MethodDelete, "/my-bucket/invalid-key", nil),
- },
- wantErr: false,
- statusCode: 404,
- },
- }
- for _, tt := range tests {
- resp, err := tt.app.Test(tt.args.req)
-
- if (err != nil) != tt.wantErr {
- t.Errorf("S3ApiController.DeleteActions() error = %v, wantErr %v", err, tt.wantErr)
- }
-
- if resp.StatusCode != tt.statusCode {
- t.Errorf("S3ApiController.DeleteActions() statusCode = %v, wantStatusCode = %v", resp.StatusCode, tt.statusCode)
- }
- }
-}
-
-func TestS3ApiController_HeadBucket(t *testing.T) {
- type args struct {
- req *http.Request
- }
-
- app := fiber.New()
- s3ApiController := S3ApiController{
- be: &BackendMock{
- GetBucketAclFunc: func(context.Context, *s3.GetBucketAclInput) ([]byte, error) {
- return acldata, nil
- },
- HeadBucketFunc: func(context.Context, *s3.HeadBucketInput) (*s3.HeadBucketOutput, error) {
- return &s3.HeadBucketOutput{}, nil
- },
- },
- }
-
- app.Use(func(ctx *fiber.Ctx) error {
- utils.ContextKeyAccount.Set(ctx, auth.Account{Access: "valid access"})
- utils.ContextKeyIsRoot.Set(ctx, true)
- utils.ContextKeyParsedAcl.Set(ctx, auth.ACL{})
- utils.ContextKeyRegion.Set(ctx, "us-east-1")
- return ctx.Next()
- })
-
- app.Head("/:bucket", s3ApiController.HeadBucket)
-
- // Error case
- appErr := fiber.New()
-
- s3ApiControllerErr := S3ApiController{be: &BackendMock{
- GetBucketAclFunc: func(context.Context, *s3.GetBucketAclInput) ([]byte, error) {
- return acldata, nil
- },
- HeadBucketFunc: func(context.Context, *s3.HeadBucketInput) (*s3.HeadBucketOutput, error) {
- return nil, s3err.GetAPIError(s3err.ErrBucketNotEmpty)
- },
- },
- }
-
- appErr.Use(func(ctx *fiber.Ctx) error {
- utils.ContextKeyAccount.Set(ctx, auth.Account{Access: "valid access"})
- utils.ContextKeyIsRoot.Set(ctx, true)
- utils.ContextKeyParsedAcl.Set(ctx, auth.ACL{})
- utils.ContextKeyRegion.Set(ctx, "us-east-1")
- return ctx.Next()
- })
-
- appErr.Head("/:bucket", s3ApiControllerErr.HeadBucket)
-
- tests := []struct {
- name string
- app *fiber.App
- args args
- wantErr bool
- statusCode int
- }{
- {
- name: "Head-bucket-success",
- app: app,
- args: args{
- req: httptest.NewRequest(http.MethodHead, "/my-bucket", nil),
- },
- wantErr: false,
- statusCode: 200,
- },
- {
- name: "Head-bucket-error",
- app: appErr,
- args: args{
- req: httptest.NewRequest(http.MethodHead, "/my-bucket", nil),
- },
- wantErr: false,
- statusCode: 409,
- },
- }
- for _, tt := range tests {
- resp, err := tt.app.Test(tt.args.req)
-
- if (err != nil) != tt.wantErr {
- t.Errorf("S3ApiController.HeadBucket() error = %v, wantErr %v", err, tt.wantErr)
- }
-
- if resp.StatusCode != tt.statusCode {
- t.Errorf("S3ApiController.HeadBucket() statusCode = %v, wantStatusCode = %v", resp.StatusCode, tt.statusCode)
- }
- }
-}
-
-func TestS3ApiController_HeadObject(t *testing.T) {
- type args struct {
- req *http.Request
- }
-
- app := fiber.New()
-
- // Mock values
- contentEncoding := "gzip"
- contentType := "application/xml"
- eTag := "Valid etag"
- lastModifie := time.Now()
- contentLength := int64(64)
-
- s3ApiController := S3ApiController{
- be: &BackendMock{
- GetBucketAclFunc: func(context.Context, *s3.GetBucketAclInput) ([]byte, error) {
- return acldata, nil
- },
- HeadObjectFunc: func(context.Context, *s3.HeadObjectInput) (*s3.HeadObjectOutput, error) {
- return &s3.HeadObjectOutput{
- ContentEncoding: &contentEncoding,
- ContentLength: &contentLength,
- ContentType: &contentType,
- LastModified: &lastModifie,
- ETag: &eTag,
- }, nil
- },
- },
- }
-
- app.Use(func(ctx *fiber.Ctx) error {
- utils.ContextKeyAccount.Set(ctx, auth.Account{Access: "valid access"})
- utils.ContextKeyIsRoot.Set(ctx, true)
- utils.ContextKeyParsedAcl.Set(ctx, auth.ACL{})
- return ctx.Next()
- })
- app.Head("/:bucket/:key/*", s3ApiController.HeadObject)
-
- //Error case
- appErr := fiber.New()
-
- s3ApiControllerErr := S3ApiController{
- be: &BackendMock{
- GetBucketAclFunc: func(context.Context, *s3.GetBucketAclInput) ([]byte, error) {
- return acldata, nil
- },
- HeadObjectFunc: func(context.Context, *s3.HeadObjectInput) (*s3.HeadObjectOutput, error) {
- return nil, s3err.GetAPIError(s3err.ErrInvalidRequest)
- },
- },
- }
-
- appErr.Use(func(ctx *fiber.Ctx) error {
- utils.ContextKeyAccount.Set(ctx, auth.Account{Access: "valid access"})
- utils.ContextKeyIsRoot.Set(ctx, true)
- utils.ContextKeyParsedAcl.Set(ctx, auth.ACL{})
- return ctx.Next()
- })
- appErr.Head("/:bucket/:key/*", s3ApiControllerErr.HeadObject)
-
- invChecksumMode := httptest.NewRequest(http.MethodHead, "/my-bucket/my-key", nil)
- invChecksumMode.Header.Set("X-Amz-Checksum-Mode", "invalid_checksum_mode")
-
- tests := []struct {
- name string
- app *fiber.App
- args args
- wantErr bool
- statusCode int
- }{
- {
- name: "Head-object-success",
- app: app,
- args: args{
- req: httptest.NewRequest(http.MethodHead, "/my-bucket/my-key", nil),
- },
- wantErr: false,
- statusCode: 200,
- },
- {
- name: "Head-object-invalid-checksum-mode",
- app: app,
- args: args{
- req: invChecksumMode,
- },
- wantErr: false,
- statusCode: 400,
- },
- {
- name: "Head-object-error",
- app: appErr,
- args: args{
- req: httptest.NewRequest(http.MethodHead, "/my-bucket/my-key", nil),
- },
- wantErr: false,
- statusCode: 400,
- },
- }
- for _, tt := range tests {
- resp, err := tt.app.Test(tt.args.req)
-
- if (err != nil) != tt.wantErr {
- t.Errorf("S3ApiController.HeadObject() error = %v, wantErr %v", err, tt.wantErr)
- }
-
- if resp.StatusCode != tt.statusCode {
- t.Errorf("S3ApiController.HeadObject() statusCode = %v, wantStatusCode = %v", resp.StatusCode, tt.statusCode)
- }
- }
-}
-
-func TestS3ApiController_CreateActions(t *testing.T) {
- type args struct {
- req *http.Request
- }
- app := fiber.New()
- s3ApiController := S3ApiController{
- be: &BackendMock{
- GetBucketAclFunc: func(context.Context, *s3.GetBucketAclInput) ([]byte, error) {
- return acldata, nil
- },
- RestoreObjectFunc: func(context.Context, *s3.RestoreObjectInput) error {
- return nil
- },
- CompleteMultipartUploadFunc: func(context.Context, *s3.CompleteMultipartUploadInput) (s3response.CompleteMultipartUploadResult, string, error) {
- return s3response.CompleteMultipartUploadResult{}, "", nil
- },
- CreateMultipartUploadFunc: func(context.Context, s3response.CreateMultipartUploadInput) (s3response.InitiateMultipartUploadResult, error) {
- return s3response.InitiateMultipartUploadResult{}, nil
- },
- SelectObjectContentFunc: func(context.Context, *s3.SelectObjectContentInput) func(w *bufio.Writer) {
- return func(w *bufio.Writer) {}
- },
- },
- }
-
- bdy := `
-
- string
- string
-
- `
-
- completMpBody := `
-
-
- etag
- 1
-
-
- `
-
- completMpEmptyBody := `
-
- `
-
- app.Use(func(ctx *fiber.Ctx) error {
- utils.ContextKeyAccount.Set(ctx, auth.Account{Access: "valid access"})
- utils.ContextKeyIsRoot.Set(ctx, true)
- utils.ContextKeyParsedAcl.Set(ctx, auth.ACL{})
- return ctx.Next()
- })
- app.Post("/:bucket/:key/*", s3ApiController.CreateActions)
-
- invChecksumAlgo := httptest.NewRequest(http.MethodPost, "/my-bucket/my-key", nil)
- invChecksumAlgo.Header.Set("X-Amz-Checksum-Algorithm", "invalid_checksum_algorithm")
-
- tests := []struct {
- name string
- app *fiber.App
- args args
- wantErr bool
- statusCode int
- }{
- {
- name: "Restore-object-success",
- app: app,
- args: args{
- req: httptest.NewRequest(http.MethodPost, "/my-bucket/my-key?restore", nil),
- },
- wantErr: false,
- statusCode: 200,
- },
- {
- name: "Select-object-content-invalid-body",
- app: app,
- args: args{
- req: httptest.NewRequest(http.MethodPost, "/my-bucket/my-key?select&select-type=2", nil),
- },
- wantErr: false,
- statusCode: 400,
- },
- {
- name: "Select-object-content-invalid-body",
- app: app,
- args: args{
- req: httptest.NewRequest(http.MethodPost, "/my-bucket/my-key?select&select-type=2", strings.NewReader(bdy)),
- },
- wantErr: false,
- statusCode: 200,
- },
- {
- name: "Complete-multipart-upload-error",
- app: app,
- args: args{
- req: httptest.NewRequest(http.MethodPost, "/my-bucket/my-key?uploadId=23423", nil),
- },
- wantErr: false,
- statusCode: 400,
- },
- {
- name: "Complete-multipart-upload-empty-parts",
- app: app,
- args: args{
- req: httptest.NewRequest(http.MethodPost, "/my-bucket/my-key?uploadId=23423", strings.NewReader(completMpEmptyBody)),
- },
- wantErr: false,
- statusCode: 400,
- },
- {
- name: "Complete-multipart-upload-success",
- app: app,
- args: args{
- req: httptest.NewRequest(http.MethodPost, "/my-bucket/my-key?uploadId=23423", strings.NewReader(completMpBody)),
- },
- wantErr: false,
- statusCode: 200,
- },
- {
- name: "Create-multipart-upload-invalid-checksum-algorithm",
- app: app,
- args: args{
- req: invChecksumAlgo,
- },
- wantErr: false,
- statusCode: 400,
- },
- {
- name: "Create-multipart-upload-success",
- app: app,
- args: args{
- req: httptest.NewRequest(http.MethodPost, "/my-bucket/my-key", nil),
- },
- wantErr: false,
- statusCode: 200,
- },
- }
- for _, tt := range tests {
- resp, err := tt.app.Test(tt.args.req)
-
- if (err != nil) != tt.wantErr {
- t.Errorf("S3ApiController.CreateActions() error = %v, wantErr %v", err, tt.wantErr)
- }
-
- if resp.StatusCode != tt.statusCode {
- t.Errorf("S3ApiController.CreateActions() statusCode = %v, wantStatusCode = %v", resp.StatusCode, tt.statusCode)
- }
- }
-}
-
-func Test_XMLresponse(t *testing.T) {
- type args struct {
- ctx *fiber.Ctx
- resp any
- err error
- }
-
- app := fiber.New()
- ctx := app.AcquireCtx(&fasthttp.RequestCtx{})
-
- tests := []struct {
- name string
- args args
- wantErr bool
- statusCode int
- }{
- {
- name: "Internal-server-error",
- args: args{
- ctx: ctx,
- resp: nil,
- err: s3err.GetAPIError(s3err.ErrInternalError),
- },
- wantErr: false,
- statusCode: 500,
- },
- {
- name: "Error-not-implemented",
- args: args{
- ctx: ctx,
- resp: nil,
- err: s3err.GetAPIError(s3err.ErrNotImplemented),
- },
- wantErr: false,
- statusCode: 501,
- },
- {
- name: "Invalid-request-body",
- args: args{
- ctx: ctx,
- resp: make(chan int),
- err: nil,
- },
- wantErr: true,
- statusCode: 200,
- },
- {
- name: "Successful-response",
- args: args{
- ctx: ctx,
- resp: "Valid response",
- err: nil,
- },
- wantErr: false,
- statusCode: 200,
- },
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- if err := SendXMLResponse(tt.args.ctx, tt.args.resp, tt.args.err, &MetaOpts{}); (err != nil) != tt.wantErr {
- t.Errorf("response() %v error = %v, wantErr %v", tt.name, err, tt.wantErr)
- }
-
- statusCode := tt.args.ctx.Response().StatusCode()
-
- if statusCode != tt.statusCode {
- t.Errorf("response() %v code = %v, wantErr %v", tt.name, statusCode, tt.wantErr)
- }
-
- tt.args.ctx.Status(http.StatusOK)
- })
- }
-}
-
-func Test_response(t *testing.T) {
- type args struct {
- ctx *fiber.Ctx
- resp any
- err error
- opts *MetaOpts
- }
-
- app := fiber.New()
- ctx := app.AcquireCtx(&fasthttp.RequestCtx{})
-
- tests := []struct {
- name string
- args args
- wantErr bool
- statusCode int
- }{
- {
- name: "Internal-server-error",
- args: args{
- ctx: ctx,
- resp: nil,
- err: s3err.GetAPIError(s3err.ErrInternalError),
- opts: &MetaOpts{},
- },
- wantErr: false,
- statusCode: 500,
- },
- {
- name: "Internal-server-error-not-api",
- args: args{
- ctx: ctx,
- resp: nil,
- err: fmt.Errorf("custom error"),
- opts: &MetaOpts{},
- },
- wantErr: false,
- statusCode: 500,
- },
- {
- name: "Error-not-implemented",
- args: args{
- ctx: ctx,
- resp: nil,
- err: s3err.GetAPIError(s3err.ErrNotImplemented),
- opts: &MetaOpts{},
- },
- wantErr: false,
- statusCode: 501,
- },
- {
- name: "Successful-response",
- args: args{
- ctx: ctx,
- resp: "Valid response",
- err: nil,
- opts: &MetaOpts{},
- },
- wantErr: false,
- statusCode: 200,
- },
- {
- name: "Successful-response-status-204",
- args: args{
- ctx: ctx,
- resp: "Valid response",
- err: nil,
- opts: &MetaOpts{
- Status: 204,
+ headers: map[string]*string{
+ "x-amz-checksum-algorithm": utils.GetStringPtr("crc32"),
+ "x-amz-meta-key": utils.GetStringPtr("meta_key"),
+ "x-amz-mp-size": utils.GetStringPtr(""),
+ "something": nil,
},
},
- wantErr: false,
- statusCode: 204,
+ expected: map[string]string{
+ "x-amz-checksum-algorithm": "crc32",
+ "x-amz-meta-key": "meta_key",
+ },
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
- if err := SendResponse(tt.args.ctx, tt.args.err, tt.args.opts); (err != nil) != tt.wantErr {
- t.Errorf("response() %v error = %v, wantErr %v", tt.name, err, tt.wantErr)
- }
-
- statusCode := tt.args.ctx.Response().StatusCode()
-
- if statusCode != tt.statusCode {
- t.Errorf("response() %v code = %v, wantErr %v", tt.name, statusCode, tt.wantErr)
+ app := fiber.New()
+ ctx := app.AcquireCtx(&fasthttp.RequestCtx{})
+ SetResponseHeaders(ctx, tt.args.headers)
+ if tt.expected != nil {
+ for key, val := range tt.expected {
+ v := ctx.Response().Header.Peek(key)
+ assert.Equal(t, val, string(v))
+ }
}
})
}
}
+
+// mock the audit logger
+type mockAuditLogger struct {
+}
+
+func (m *mockAuditLogger) Log(_ *fiber.Ctx, _ error, _ []byte, _ s3log.LogMeta) {}
+func (m *mockAuditLogger) HangUp() error { return nil }
+func (m *mockAuditLogger) Shutdown() error { return nil }
+
+// mock S3 event sender
+type mockEvSender struct {
+}
+
+func (m *mockEvSender) SendEvent(_ *fiber.Ctx, _ s3event.EventMeta) {}
+func (m *mockEvSender) Close() error { return nil }
+
+// mock metrics manager
+
+type mockMetricsManager struct{}
+
+func (m *mockMetricsManager) Send(_ *fiber.Ctx, _ error, _ string, _ int64, _ int) {}
+func (m *mockMetricsManager) Close() {}
+
+func TestProcessController(t *testing.T) {
+ payload, err := xml.Marshal(s3response.Bucket{
+ Name: "something",
+ })
+ assert.NoError(t, err)
+
+ payloadLen := len(payload) + len(xmlhdr)
+
+ services := &Services{
+ Logger: &mockAuditLogger{},
+ EventSender: &mockEvSender{},
+ MetricsManager: &mockMetricsManager{},
+ }
+ type args struct {
+ controller Controller
+ svc *Services
+ }
+ type expected struct {
+ status int
+ headers map[string]string
+ body []byte
+ }
+ tests := []struct {
+ name string
+ args args
+ expected expected
+ }{
+ {
+ name: "no services successfull response",
+ args: args{
+ svc: &Services{},
+ controller: func(ctx *fiber.Ctx) (*Response, error) {
+ return &Response{}, nil
+ },
+ },
+ expected: expected{
+ status: http.StatusOK,
+ },
+ },
+ {
+ name: "handle api error",
+ args: args{
+ svc: services,
+ controller: func(ctx *fiber.Ctx) (*Response, error) {
+ return &Response{}, s3err.GetAPIError(s3err.ErrInvalidRequest)
+ },
+ },
+ expected: expected{
+ status: http.StatusBadRequest,
+ body: s3err.GetAPIErrorResponse(s3err.GetAPIError(s3err.ErrInvalidRequest), "", "", ""),
+ },
+ },
+ {
+ name: "handle custom error",
+ args: args{
+ svc: services,
+ controller: func(ctx *fiber.Ctx) (*Response, error) {
+ return &Response{}, errors.New("custom error")
+ },
+ },
+ expected: expected{
+ status: http.StatusInternalServerError,
+ body: s3err.GetAPIErrorResponse(s3err.GetAPIError(s3err.ErrInternalError), "", "", ""),
+ },
+ },
+ {
+ name: "body parsing fails",
+ args: args{
+ svc: services,
+ controller: func(ctx *fiber.Ctx) (*Response, error) {
+ return &Response{
+ Data: make(chan int),
+ }, nil
+ },
+ },
+ expected: expected{
+ status: http.StatusInternalServerError,
+ body: s3err.GetAPIErrorResponse(s3err.GetAPIError(s3err.ErrInternalError), "", "", ""),
+ },
+ },
+ {
+ name: "no data payload",
+ args: args{
+ svc: services,
+ controller: func(ctx *fiber.Ctx) (*Response, error) {
+ return &Response{
+ MetaOpts: &MetaOptions{
+ ObjectCount: 2,
+ },
+ }, nil
+ },
+ },
+ expected: expected{
+ status: http.StatusOK,
+ },
+ },
+ {
+ name: "should return 204 http status",
+ args: args{
+ svc: services,
+ controller: func(ctx *fiber.Ctx) (*Response, error) {
+ return &Response{
+ MetaOpts: &MetaOptions{
+ Status: http.StatusNoContent,
+ },
+ }, nil
+ },
+ },
+ expected: expected{
+ status: http.StatusNoContent,
+ },
+ },
+ {
+ name: "already encoded payload",
+ args: args{
+ svc: services,
+ controller: func(ctx *fiber.Ctx) (*Response, error) {
+ return &Response{
+ Data: []byte("encoded_data"),
+ }, nil
+ },
+ },
+ expected: expected{
+ status: http.StatusOK,
+ body: []byte("encoded_data"),
+ headers: map[string]string{
+ "Content-Length": "12",
+ },
+ },
+ },
+ {
+ name: "should set response headers",
+ args: args{
+ svc: services,
+ controller: func(ctx *fiber.Ctx) (*Response, error) {
+ return &Response{
+ Headers: map[string]*string{
+ "X-Amz-My-Custom-Header": utils.GetStringPtr("my_value"),
+ "X-Amz-Meta-My-Meta": utils.GetStringPtr("my_meta"),
+ },
+ }, nil
+ },
+ },
+ expected: expected{
+ status: http.StatusOK,
+ headers: map[string]string{
+ "X-Amz-My-Custom-Header": "my_value",
+ "X-Amz-Meta-My-Meta": "my_meta",
+ },
+ },
+ },
+ {
+ name: "large paylod: should return internal error",
+ args: args{
+ svc: services,
+ controller: func(ctx *fiber.Ctx) (*Response, error) {
+ type Item struct {
+ Value string `xml:"value"`
+ }
+
+ type payload struct {
+ Items []Item `xml:"item"`
+ }
+
+ const targetSize = 5 * 1024 * 1024 // 5 MiB
+ const itemCount = 500
+ const valueSize = targetSize / itemCount
+
+ p := payload{
+ Items: make([]Item, itemCount),
+ }
+
+ // Preallocate one shared string of desired size
+ var sb strings.Builder
+ sb.Grow(valueSize)
+ for range valueSize {
+ sb.WriteByte('A')
+ }
+ largeValue := sb.String()
+
+ for i := range p.Items {
+ p.Items[i].Value = largeValue
+ }
+
+ return &Response{
+ Data: p,
+ }, nil
+ },
+ },
+ expected: expected{
+ body: s3err.GetAPIErrorResponse(s3err.GetAPIError(s3err.ErrInternalError), "", "", ""),
+ status: http.StatusInternalServerError,
+ },
+ },
+ {
+ name: "not encoded payload",
+ args: args{
+ svc: services,
+ controller: func(ctx *fiber.Ctx) (*Response, error) {
+ return &Response{
+ Data: s3response.Bucket{
+ Name: "something",
+ },
+ }, nil
+ },
+ },
+ expected: expected{
+ headers: map[string]string{
+ "Content-Length": fmt.Sprint(payloadLen),
+ },
+ body: append(xmlhdr, payload...),
+ status: http.StatusOK,
+ },
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ ctx := fiber.New().AcquireCtx(&fasthttp.RequestCtx{})
+ err := ProcessController(ctx, tt.args.controller, metrics.ActionAbortMultipartUpload, tt.args.svc)
+ assert.NoError(t, err)
+
+ // check the status
+ assert.Equal(t, tt.expected.status, ctx.Response().StatusCode())
+
+ // check the response headers to be set
+ if tt.expected.headers != nil {
+ for key, val := range tt.expected.headers {
+ v := ctx.Response().Header.Peek(key)
+ assert.Equal(t, val, string(v))
+ }
+ }
+
+ // check the response body
+ if tt.expected.body != nil {
+ assert.Equal(t, tt.expected.body, ctx.Response().Body())
+ }
+ })
+ }
+}
+
+func TestProcessHandlers(t *testing.T) {
+ payload, err := xml.Marshal(s3response.Checksum{
+ CRC32: utils.GetStringPtr("crc32"),
+ })
+ assert.NoError(t, err)
+
+ type args struct {
+ controller Controller
+ svc *Services
+ handlers []fiber.Handler
+ locals map[utils.ContextKey]any
+ }
+ type expected struct {
+ body []byte
+ }
+ tests := []struct {
+ name string
+ args args
+ expected expected
+ }{
+ {
+ name: "should skip the handlers",
+ args: args{
+ locals: map[utils.ContextKey]any{
+ utils.ContextKeySkip: true,
+ },
+ },
+ },
+ {
+ name: "handler returns error",
+ args: args{
+ handlers: []fiber.Handler{
+ func(ctx *fiber.Ctx) error {
+ return nil
+ },
+ func(ctx *fiber.Ctx) error {
+ return s3err.GetAPIError(s3err.ErrAccessDenied)
+ },
+ },
+ svc: &Services{},
+ },
+ expected: expected{
+ body: s3err.GetAPIErrorResponse(s3err.GetAPIError(s3err.ErrAccessDenied), "", "", ""),
+ },
+ },
+ {
+ name: "should process the controller",
+ args: args{
+ handlers: []fiber.Handler{
+ func(ctx *fiber.Ctx) error {
+ return nil
+ },
+ func(ctx *fiber.Ctx) error {
+ return nil
+ },
+ },
+ svc: &Services{},
+ controller: func(ctx *fiber.Ctx) (*Response, error) {
+ return &Response{
+ Data: s3response.Checksum{
+ CRC32: utils.GetStringPtr("crc32"),
+ },
+ }, nil
+ },
+ },
+ expected: expected{
+ body: append(xmlhdr, payload...),
+ },
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ mdlwr := ProcessHandlers(tt.args.controller, metrics.ActionCreateBucket, tt.args.svc, tt.args.handlers...)
+
+ app := fiber.New()
+
+ app.Post("/:bucket/*", func(ctx *fiber.Ctx) error {
+ // set the request locals
+ if tt.args.locals != nil {
+ for key, val := range tt.args.locals {
+ key.Set(ctx, val)
+ }
+ }
+
+ // call the controller by passing the ctx
+ err := mdlwr(ctx)
+ assert.NoError(t, err)
+
+ // check the response body
+ if tt.expected.body != nil {
+ assert.Equal(t, tt.expected.body, ctx.Response().Body())
+ }
+
+ return nil
+ })
+
+ app.All("*", func(ctx *fiber.Ctx) error {
+ return nil
+ })
+
+ req := buildRequest("bucket", "object", nil, nil, nil)
+
+ _, err := app.Test(req)
+ assert.NoError(t, err)
+ })
+ }
+}
+
+func TestWrapMiddleware(t *testing.T) {
+ type args struct {
+ handler fiber.Handler
+ logger s3log.AuditLogger
+ mm metrics.Manager
+ }
+ type expected struct {
+ body []byte
+ }
+ tests := []struct {
+ name string
+ args args
+ expected expected
+ }{
+ {
+ name: "handler returns no error",
+ args: args{
+ handler: func(ctx *fiber.Ctx) error {
+ return nil
+ },
+ },
+ },
+ {
+ name: "handler returns api error",
+ args: args{
+ handler: func(ctx *fiber.Ctx) error {
+ return s3err.GetAPIError(s3err.ErrAclNotSupported)
+ },
+ mm: &mockMetricsManager{},
+ logger: &mockAuditLogger{},
+ },
+ expected: expected{
+ body: s3err.GetAPIErrorResponse(s3err.GetAPIError(s3err.ErrAclNotSupported), "", "", ""),
+ },
+ },
+ {
+ name: "handler returns custom error",
+ args: args{
+ handler: func(ctx *fiber.Ctx) error {
+ return errors.New("custom error")
+ },
+ },
+ expected: expected{
+ body: s3err.GetAPIErrorResponse(s3err.GetAPIError(s3err.ErrInternalError), "", "", ""),
+ },
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ mdlwr := WrapMiddleware(tt.args.handler, tt.args.logger, tt.args.mm)
+ app := fiber.New()
+
+ app.Post("/:bucket/*", func(ctx *fiber.Ctx) error {
+ // call the controller by passing the ctx
+ err := mdlwr(ctx)
+ assert.NoError(t, err)
+
+ // check the response body
+ if tt.expected.body != nil {
+ assert.Equal(t, tt.expected.body, ctx.Response().Body())
+ }
+
+ return nil
+ })
+
+ app.All("*", func(ctx *fiber.Ctx) error {
+ return nil
+ })
+
+ req := buildRequest("bucket", "object", nil, nil, nil)
+
+ _, err := app.Test(req)
+ assert.NoError(t, err)
+ })
+ }
+}
diff --git a/s3api/controllers/bucket-delete.go b/s3api/controllers/bucket-delete.go
new file mode 100644
index 00000000..22ef9286
--- /dev/null
+++ b/s3api/controllers/bucket-delete.go
@@ -0,0 +1,194 @@
+// Copyright 2023 Versity Software
+// This file is licensed under the Apache License, Version 2.0
+// (the "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package controllers
+
+import (
+ "net/http"
+
+ "github.com/gofiber/fiber/v2"
+ "github.com/versity/versitygw/auth"
+ "github.com/versity/versitygw/s3api/utils"
+)
+
+func (c S3ApiController) DeleteBucketTagging(ctx *fiber.Ctx) (*Response, error) {
+ bucket := ctx.Params("bucket")
+ acct := utils.ContextKeyAccount.Get(ctx).(auth.Account)
+ isRoot := utils.ContextKeyIsRoot.Get(ctx).(bool)
+ parsedAcl := utils.ContextKeyParsedAcl.Get(ctx).(auth.ACL)
+ IsBucketPublic := 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: IsBucketPublic,
+ })
+ if err != nil {
+ return &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: parsedAcl.Owner,
+ },
+ }, err
+ }
+
+ err = c.be.DeleteBucketTagging(ctx.Context(), bucket)
+ return &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: parsedAcl.Owner,
+ Status: http.StatusNoContent,
+ },
+ }, err
+}
+
+func (c S3ApiController) DeleteBucketOwnershipControls(ctx *fiber.Ctx) (*Response, error) {
+ bucket := ctx.Params("bucket")
+ acct := utils.ContextKeyAccount.Get(ctx).(auth.Account)
+ isRoot := utils.ContextKeyIsRoot.Get(ctx).(bool)
+ 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,
+ Action: auth.PutBucketOwnershipControlsAction,
+ })
+ if err != nil {
+ return &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: parsedAcl.Owner,
+ },
+ }, err
+ }
+
+ err = c.be.DeleteBucketOwnershipControls(ctx.Context(), bucket)
+ return &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: parsedAcl.Owner,
+ Status: http.StatusNoContent,
+ },
+ }, err
+}
+
+func (c S3ApiController) DeleteBucketPolicy(ctx *fiber.Ctx) (*Response, error) {
+ bucket := ctx.Params("bucket")
+ acct := utils.ContextKeyAccount.Get(ctx).(auth.Account)
+ isRoot := utils.ContextKeyIsRoot.Get(ctx).(bool)
+ 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,
+ Action: auth.DeleteBucketPolicyAction,
+ })
+ if err != nil {
+ return &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: parsedAcl.Owner,
+ },
+ }, err
+ }
+
+ err = c.be.DeleteBucketPolicy(ctx.Context(), bucket)
+ return &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: parsedAcl.Owner,
+ Status: http.StatusNoContent,
+ },
+ }, err
+}
+
+func (c S3ApiController) DeleteBucketCors(ctx *fiber.Ctx) (*Response, error) {
+ bucket := ctx.Params("bucket")
+ acct := utils.ContextKeyAccount.Get(ctx).(auth.Account)
+ isRoot := utils.ContextKeyIsRoot.Get(ctx).(bool)
+ parsedAcl := utils.ContextKeyParsedAcl.Get(ctx).(auth.ACL)
+ IsBucketPublic := 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: IsBucketPublic,
+ })
+ if err != nil {
+ return &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: parsedAcl.Owner,
+ },
+ }, err
+ }
+
+ err = c.be.DeleteBucketCors(ctx.Context(), bucket)
+ return &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: parsedAcl.Owner,
+ Status: http.StatusNoContent,
+ },
+ }, err
+}
+
+func (c S3ApiController) DeleteBucket(ctx *fiber.Ctx) (*Response, error) {
+ bucket := ctx.Params("bucket")
+ acct := utils.ContextKeyAccount.Get(ctx).(auth.Account)
+ isRoot := utils.ContextKeyIsRoot.Get(ctx).(bool)
+ parsedAcl := utils.ContextKeyParsedAcl.Get(ctx).(auth.ACL)
+ IsBucketPublic := 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.DeleteBucketAction,
+ IsBucketPublic: IsBucketPublic,
+ })
+ if err != nil {
+ return &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: parsedAcl.Owner,
+ },
+ }, err
+ }
+
+ err = c.be.DeleteBucket(ctx.Context(), bucket)
+ return &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: parsedAcl.Owner,
+ Status: http.StatusNoContent,
+ },
+ }, err
+}
diff --git a/s3api/controllers/bucket-delete_test.go b/s3api/controllers/bucket-delete_test.go
new file mode 100644
index 00000000..50ae11d9
--- /dev/null
+++ b/s3api/controllers/bucket-delete_test.go
@@ -0,0 +1,413 @@
+// Copyright 2023 Versity Software
+// This file is licensed under the Apache License, Version 2.0
+// (the "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package controllers
+
+import (
+ "context"
+ "net/http"
+ "testing"
+
+ "github.com/versity/versitygw/s3err"
+)
+
+func TestS3ApiController_DeleteBucketTagging(t *testing.T) {
+ tests := []struct {
+ name string
+ input testInput
+ output testOutput
+ }{
+ {
+ name: "verify access fails",
+ input: testInput{
+ locals: accessDeniedLocals,
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ },
+ },
+ err: s3err.GetAPIError(s3err.ErrAccessDenied),
+ },
+ },
+ {
+ name: "backend returns error",
+ input: testInput{
+ locals: defaultLocals,
+ beErr: s3err.GetAPIError(s3err.ErrAclNotSupported),
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ Status: http.StatusNoContent,
+ },
+ },
+ err: s3err.GetAPIError(s3err.ErrAclNotSupported),
+ },
+ },
+ {
+ name: "successful response",
+ input: testInput{
+ locals: defaultLocals,
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ Status: http.StatusNoContent,
+ },
+ },
+ },
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ be := &BackendMock{
+ DeleteBucketTaggingFunc: func(_ context.Context, _ string) error {
+ return tt.input.beErr
+ },
+ GetBucketPolicyFunc: func(contextMoqParam context.Context, bucket string) ([]byte, error) {
+ return nil, s3err.GetAPIError(s3err.ErrAccessDenied)
+ },
+ }
+
+ ctrl := S3ApiController{
+ be: be,
+ }
+
+ testController(
+ t,
+ ctrl.DeleteBucketTagging,
+ tt.output.response,
+ tt.output.err,
+ ctxInputs{
+ locals: tt.input.locals,
+ })
+ })
+ }
+}
+
+func TestS3ApiController_DeleteBucketOwnershipControls(t *testing.T) {
+ tests := []struct {
+ name string
+ input testInput
+ output testOutput
+ }{
+ {
+ name: "verify access fails",
+ input: testInput{
+ locals: accessDeniedLocals,
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ },
+ },
+ err: s3err.GetAPIError(s3err.ErrAccessDenied),
+ },
+ },
+ {
+ name: "backend returns error",
+ input: testInput{
+ locals: defaultLocals,
+ beErr: s3err.GetAPIError(s3err.ErrInvalidAccessKeyID),
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ Status: http.StatusNoContent,
+ },
+ },
+ err: s3err.GetAPIError(s3err.ErrInvalidAccessKeyID),
+ },
+ },
+ {
+ name: "successful response",
+ input: testInput{
+ locals: defaultLocals,
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ Status: http.StatusNoContent,
+ },
+ },
+ },
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ be := &BackendMock{
+ DeleteBucketOwnershipControlsFunc: func(contextMoqParam context.Context, bucket string) error {
+ return tt.input.beErr
+ },
+ GetBucketPolicyFunc: func(contextMoqParam context.Context, bucket string) ([]byte, error) {
+ return nil, s3err.GetAPIError(s3err.ErrAccessDenied)
+ },
+ }
+
+ ctrl := S3ApiController{
+ be: be,
+ }
+
+ testController(
+ t,
+ ctrl.DeleteBucketOwnershipControls,
+ tt.output.response,
+ tt.output.err,
+ ctxInputs{
+ locals: tt.input.locals,
+ })
+ })
+ }
+}
+
+func TestS3ApiController_DeleteBucketPolicy(t *testing.T) {
+ tests := []struct {
+ name string
+ input testInput
+ output testOutput
+ }{
+ {
+ name: "verify access fails",
+ input: testInput{
+ locals: accessDeniedLocals,
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ },
+ },
+ err: s3err.GetAPIError(s3err.ErrAccessDenied),
+ },
+ },
+ {
+ name: "backend returns error",
+ input: testInput{
+ locals: defaultLocals,
+ beErr: s3err.GetAPIError(s3err.ErrInvalidDigest),
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ Status: http.StatusNoContent,
+ },
+ },
+ err: s3err.GetAPIError(s3err.ErrInvalidDigest),
+ },
+ },
+ {
+ name: "successful response",
+ input: testInput{
+ locals: defaultLocals,
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ Status: http.StatusNoContent,
+ },
+ },
+ },
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ be := &BackendMock{
+ DeleteBucketPolicyFunc: func(contextMoqParam context.Context, bucket string) error {
+ return tt.input.beErr
+ },
+ GetBucketPolicyFunc: func(contextMoqParam context.Context, bucket string) ([]byte, error) {
+ return nil, s3err.GetAPIError(s3err.ErrAccessDenied)
+ },
+ }
+
+ ctrl := S3ApiController{
+ be: be,
+ }
+
+ testController(
+ t,
+ ctrl.DeleteBucketPolicy,
+ tt.output.response,
+ tt.output.err,
+ ctxInputs{
+ locals: tt.input.locals,
+ })
+ })
+ }
+}
+
+func TestS3ApiController_DeleteBucketCors(t *testing.T) {
+ tests := []struct {
+ name string
+ input testInput
+ output testOutput
+ }{
+ {
+ name: "verify access fails",
+ input: testInput{
+ locals: accessDeniedLocals,
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ },
+ },
+ err: s3err.GetAPIError(s3err.ErrAccessDenied),
+ },
+ },
+ {
+ name: "backend returns error",
+ input: testInput{
+ locals: defaultLocals,
+ beErr: s3err.GetAPIError(s3err.ErrAdminMethodNotSupported),
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ Status: http.StatusNoContent,
+ },
+ },
+ err: s3err.GetAPIError(s3err.ErrAdminMethodNotSupported),
+ },
+ },
+ {
+ name: "successful response",
+ input: testInput{
+ locals: defaultLocals,
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ Status: http.StatusNoContent,
+ },
+ },
+ },
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ be := &BackendMock{
+ DeleteBucketCorsFunc: func(contextMoqParam context.Context, bucket string) error {
+ return tt.input.beErr
+ },
+ GetBucketPolicyFunc: func(contextMoqParam context.Context, bucket string) ([]byte, error) {
+ return nil, s3err.GetAPIError(s3err.ErrAccessDenied)
+ },
+ }
+
+ ctrl := S3ApiController{
+ be: be,
+ }
+
+ testController(
+ t,
+ ctrl.DeleteBucketCors,
+ tt.output.response,
+ tt.output.err,
+ ctxInputs{
+ locals: tt.input.locals,
+ })
+ })
+ }
+}
+
+func TestS3ApiController_DeleteBucket(t *testing.T) {
+ tests := []struct {
+ name string
+ input testInput
+ output testOutput
+ }{
+ {
+ name: "verify access fails",
+ input: testInput{
+ locals: accessDeniedLocals,
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ },
+ },
+ err: s3err.GetAPIError(s3err.ErrAccessDenied),
+ },
+ },
+ {
+ name: "backend returns error",
+ input: testInput{
+ locals: defaultLocals,
+ beErr: s3err.GetAPIError(s3err.ErrInvalidDigest),
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ Status: http.StatusNoContent,
+ },
+ },
+ err: s3err.GetAPIError(s3err.ErrInvalidDigest),
+ },
+ },
+ {
+ name: "successful response",
+ input: testInput{
+ locals: defaultLocals,
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ Status: http.StatusNoContent,
+ },
+ },
+ },
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ be := &BackendMock{
+ DeleteBucketFunc: func(contextMoqParam context.Context, bucket string) error {
+ return tt.input.beErr
+ },
+ GetBucketPolicyFunc: func(contextMoqParam context.Context, bucket string) ([]byte, error) {
+ return nil, s3err.GetAPIError(s3err.ErrAccessDenied)
+ },
+ }
+
+ ctrl := S3ApiController{
+ be: be,
+ }
+
+ testController(
+ t,
+ ctrl.DeleteBucket,
+ tt.output.response,
+ tt.output.err,
+ ctxInputs{
+ locals: tt.input.locals,
+ })
+ })
+ }
+}
diff --git a/s3api/controllers/bucket-get.go b/s3api/controllers/bucket-get.go
new file mode 100644
index 00000000..f4da5c34
--- /dev/null
+++ b/s3api/controllers/bucket-get.go
@@ -0,0 +1,558 @@
+// Copyright 2023 Versity Software
+// This file is licensed under the Apache License, Version 2.0
+// (the "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package controllers
+
+import (
+ "strings"
+
+ "github.com/aws/aws-sdk-go-v2/service/s3"
+ "github.com/aws/aws-sdk-go-v2/service/s3/types"
+ "github.com/gofiber/fiber/v2"
+ "github.com/versity/versitygw/auth"
+ "github.com/versity/versitygw/s3api/debuglogger"
+ "github.com/versity/versitygw/s3api/utils"
+ "github.com/versity/versitygw/s3err"
+ "github.com/versity/versitygw/s3response"
+)
+
+func (c S3ApiController) GetBucketTagging(ctx *fiber.Ctx) (*Response, error) {
+ bucket := ctx.Params("bucket")
+ acct := utils.ContextKeyAccount.Get(ctx).(auth.Account)
+ isRoot := utils.ContextKeyIsRoot.Get(ctx).(bool)
+ isPublicBucket := utils.ContextKeyPublicBucket.IsSet(ctx)
+ 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,
+ })
+ if err != nil {
+ return &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: parsedAcl.Owner,
+ },
+ }, err
+ }
+
+ tags, err := c.be.GetBucketTagging(ctx.Context(), bucket)
+ if err != nil {
+ return &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: parsedAcl.Owner,
+ },
+ }, err
+ }
+ resp := s3response.Tagging{
+ TagSet: s3response.TagSet{
+ Tags: make([]s3response.Tag, 0, len(tags)),
+ },
+ }
+
+ for key, val := range tags {
+ resp.TagSet.Tags = append(resp.TagSet.Tags,
+ s3response.Tag{Key: key, Value: val})
+ }
+
+ return &Response{
+ Data: resp,
+ MetaOpts: &MetaOptions{
+ BucketOwner: parsedAcl.Owner,
+ },
+ }, err
+}
+
+func (c S3ApiController) GetBucketOwnershipControls(ctx *fiber.Ctx) (*Response, error) {
+ bucket := ctx.Params("bucket")
+ acct := utils.ContextKeyAccount.Get(ctx).(auth.Account)
+ isRoot := utils.ContextKeyIsRoot.Get(ctx).(bool)
+ isPublicBucket := utils.ContextKeyPublicBucket.IsSet(ctx)
+ 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,
+ })
+ if err != nil {
+ return &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: parsedAcl.Owner,
+ },
+ }, err
+ }
+
+ data, err := c.be.GetBucketOwnershipControls(ctx.Context(), bucket)
+ return &Response{
+ Data: s3response.OwnershipControls{
+ Rules: []types.OwnershipControlsRule{
+ {
+ ObjectOwnership: data,
+ },
+ },
+ },
+ MetaOpts: &MetaOptions{
+ BucketOwner: parsedAcl.Owner,
+ },
+ }, err
+}
+
+func (c S3ApiController) GetBucketVersioning(ctx *fiber.Ctx) (*Response, error) {
+ bucket := ctx.Params("bucket")
+ acct := utils.ContextKeyAccount.Get(ctx).(auth.Account)
+ isRoot := utils.ContextKeyIsRoot.Get(ctx).(bool)
+ isPublicBucket := utils.ContextKeyPublicBucket.IsSet(ctx)
+ 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,
+ })
+ if err != nil {
+ return &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: parsedAcl.Owner,
+ },
+ }, err
+ }
+ // Only admin users and the bucket owner are allowed to get the versioning state of a bucket.
+ if err := auth.IsAdminOrOwner(acct, isRoot, parsedAcl); err != nil {
+ return &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: parsedAcl.Owner,
+ },
+ }, err
+ }
+
+ data, err := c.be.GetBucketVersioning(ctx.Context(), bucket)
+ return &Response{
+ Data: data,
+ MetaOpts: &MetaOptions{
+ BucketOwner: parsedAcl.Owner,
+ },
+ }, err
+}
+
+func (c S3ApiController) GetBucketCors(ctx *fiber.Ctx) (*Response, error) {
+ bucket := ctx.Params("bucket")
+ acct := utils.ContextKeyAccount.Get(ctx).(auth.Account)
+ isRoot := utils.ContextKeyIsRoot.Get(ctx).(bool)
+ isPublicBucket := utils.ContextKeyPublicBucket.IsSet(ctx)
+ 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,
+ })
+ if err != nil {
+ return &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: parsedAcl.Owner,
+ },
+ }, err
+ }
+
+ data, err := c.be.GetBucketCors(ctx.Context(), bucket)
+ return &Response{
+ Data: data,
+ MetaOpts: &MetaOptions{
+ BucketOwner: parsedAcl.Owner,
+ },
+ }, err
+}
+
+func (c S3ApiController) GetBucketPolicy(ctx *fiber.Ctx) (*Response, error) {
+ bucket := ctx.Params("bucket")
+ acct := utils.ContextKeyAccount.Get(ctx).(auth.Account)
+ isRoot := utils.ContextKeyIsRoot.Get(ctx).(bool)
+ isPublicBucket := utils.ContextKeyPublicBucket.IsSet(ctx)
+ 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,
+ })
+ if err != nil {
+ return &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: parsedAcl.Owner,
+ },
+ }, err
+ }
+
+ data, err := c.be.GetBucketPolicy(ctx.Context(), bucket)
+ return &Response{
+ Data: data,
+ MetaOpts: &MetaOptions{
+ BucketOwner: parsedAcl.Owner,
+ },
+ }, err
+}
+
+func (c S3ApiController) ListObjectVersions(ctx *fiber.Ctx) (*Response, error) {
+ // url values
+ bucket := ctx.Params("bucket")
+ prefix := ctx.Query("prefix")
+ delimiter := ctx.Query("delimiter")
+ maxkeysStr := ctx.Query("max-keys")
+ keyMarker := ctx.Query("key-marker")
+ versionIdMarker := ctx.Query("version-id-marker")
+ // context keys
+ acct := utils.ContextKeyAccount.Get(ctx).(auth.Account)
+ isRoot := utils.ContextKeyIsRoot.Get(ctx).(bool)
+ isPublicBucket := utils.ContextKeyPublicBucket.IsSet(ctx)
+ 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,
+ })
+ if err != nil {
+ return &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: parsedAcl.Owner,
+ },
+ }, err
+ }
+
+ maxkeys, err := utils.ParseUint(maxkeysStr)
+ if err != nil {
+ debuglogger.Logf("error parsing max keys %q: %v",
+ maxkeysStr, err)
+ return &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: parsedAcl.Owner,
+ },
+ }, s3err.GetAPIError(s3err.ErrInvalidMaxKeys)
+ }
+
+ data, err := c.be.ListObjectVersions(ctx.Context(),
+ &s3.ListObjectVersionsInput{
+ Bucket: &bucket,
+ Delimiter: &delimiter,
+ KeyMarker: &keyMarker,
+ MaxKeys: &maxkeys,
+ Prefix: &prefix,
+ VersionIdMarker: &versionIdMarker,
+ })
+ return &Response{
+ Data: data,
+ MetaOpts: &MetaOptions{
+ BucketOwner: parsedAcl.Owner,
+ },
+ }, err
+}
+
+func (c S3ApiController) GetObjectLockConfiguration(ctx *fiber.Ctx) (*Response, error) {
+ // url values
+ bucket := ctx.Params("bucket")
+ // context keys
+ acct := utils.ContextKeyAccount.Get(ctx).(auth.Account)
+ isRoot := utils.ContextKeyIsRoot.Get(ctx).(bool)
+ isPublicBucket := utils.ContextKeyPublicBucket.IsSet(ctx)
+ 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,
+ })
+ if err != nil {
+ return &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: parsedAcl.Owner,
+ },
+ }, err
+ }
+
+ data, err := c.be.GetObjectLockConfiguration(ctx.Context(), bucket)
+ if err != nil {
+ return &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: parsedAcl.Owner,
+ },
+ }, err
+ }
+
+ resp, err := auth.ParseBucketLockConfigurationOutput(data)
+ return &Response{
+ Data: resp,
+ MetaOpts: &MetaOptions{
+ BucketOwner: parsedAcl.Owner,
+ },
+ }, err
+}
+
+func (c S3ApiController) GetBucketAcl(ctx *fiber.Ctx) (*Response, error) {
+ // url values
+ bucket := ctx.Params("bucket")
+ // context keys
+ acct := utils.ContextKeyAccount.Get(ctx).(auth.Account)
+ isRoot := utils.ContextKeyIsRoot.Get(ctx).(bool)
+ isPublicBucket := utils.ContextKeyPublicBucket.IsSet(ctx)
+ 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,
+ })
+ if err != nil {
+ return &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: parsedAcl.Owner,
+ },
+ }, err
+ }
+
+ data, err := c.be.GetBucketAcl(ctx.Context(),
+ &s3.GetBucketAclInput{Bucket: &bucket})
+ if err != nil {
+ return &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: parsedAcl.Owner,
+ },
+ }, err
+ }
+
+ res, err := auth.ParseACLOutput(data, parsedAcl.Owner)
+ return &Response{
+ Data: res,
+ MetaOpts: &MetaOptions{
+ BucketOwner: parsedAcl.Owner,
+ },
+ }, err
+}
+
+func (c S3ApiController) ListMultipartUploads(ctx *fiber.Ctx) (*Response, error) {
+ // url values
+ bucket := ctx.Params("bucket")
+ prefix := ctx.Query("prefix")
+ delimiter := ctx.Query("delimiter")
+ keyMarker := ctx.Query("key-marker")
+ maxUploadsStr := ctx.Query("max-uploads")
+ uploadIdMarker := ctx.Query("upload-id-marker")
+ // context keys
+ acct := utils.ContextKeyAccount.Get(ctx).(auth.Account)
+ isRoot := utils.ContextKeyIsRoot.Get(ctx).(bool)
+ isPublicBucket := utils.ContextKeyPublicBucket.IsSet(ctx)
+ 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,
+ })
+ if err != nil {
+ return &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: parsedAcl.Owner,
+ },
+ }, err
+ }
+ maxUploads, err := utils.ParseUint(maxUploadsStr)
+ if err != nil {
+ debuglogger.Logf("error parsing max uploads %q: %v",
+ maxUploadsStr, err)
+ return &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: parsedAcl.Owner,
+ },
+ }, s3err.GetAPIError(s3err.ErrInvalidMaxUploads)
+ }
+ res, err := c.be.ListMultipartUploads(ctx.Context(),
+ &s3.ListMultipartUploadsInput{
+ Bucket: &bucket,
+ Delimiter: &delimiter,
+ Prefix: &prefix,
+ UploadIdMarker: &uploadIdMarker,
+ MaxUploads: &maxUploads,
+ KeyMarker: &keyMarker,
+ })
+ return &Response{
+ Data: res,
+ MetaOpts: &MetaOptions{
+ BucketOwner: parsedAcl.Owner,
+ },
+ }, err
+}
+
+func (c S3ApiController) ListObjectsV2(ctx *fiber.Ctx) (*Response, error) {
+ // url values
+ bucket := ctx.Params("bucket")
+ prefix := ctx.Query("prefix")
+ cToken := ctx.Query("continuation-token")
+ sAfter := ctx.Query("start-after")
+ delimiter := ctx.Query("delimiter")
+ maxkeysStr := ctx.Query("max-keys")
+ fetchOwner := strings.EqualFold(ctx.Query("fetch-owner"), "true")
+ // context locals
+ acct := utils.ContextKeyAccount.Get(ctx).(auth.Account)
+ isRoot := utils.ContextKeyIsRoot.Get(ctx).(bool)
+ isPublicBucket := utils.ContextKeyPublicBucket.IsSet(ctx)
+ 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,
+ })
+ if err != nil {
+ return &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: parsedAcl.Owner,
+ },
+ }, err
+ }
+ maxkeys, err := utils.ParseUint(maxkeysStr)
+ if err != nil {
+ debuglogger.Logf("error parsing max keys %q: %v",
+ maxkeysStr, err)
+ return &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: parsedAcl.Owner,
+ },
+ }, s3err.GetAPIError(s3err.ErrInvalidMaxKeys)
+ }
+
+ res, err := c.be.ListObjectsV2(ctx.Context(),
+ &s3.ListObjectsV2Input{
+ Bucket: &bucket,
+ Prefix: &prefix,
+ ContinuationToken: &cToken,
+ Delimiter: &delimiter,
+ MaxKeys: &maxkeys,
+ StartAfter: &sAfter,
+ FetchOwner: &fetchOwner,
+ })
+ return &Response{
+ Data: res,
+ MetaOpts: &MetaOptions{
+ BucketOwner: parsedAcl.Owner,
+ },
+ }, err
+}
+
+func (c S3ApiController) ListObjects(ctx *fiber.Ctx) (*Response, error) {
+ // url values
+ bucket := ctx.Params("bucket")
+ prefix := ctx.Query("prefix")
+ marker := ctx.Query("marker")
+ delimiter := ctx.Query("delimiter")
+ maxkeysStr := ctx.Query("max-keys")
+ // context locals
+ acct := utils.ContextKeyAccount.Get(ctx).(auth.Account)
+ isRoot := utils.ContextKeyIsRoot.Get(ctx).(bool)
+ isPublicBucket := utils.ContextKeyPublicBucket.IsSet(ctx)
+ 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,
+ })
+ if err != nil {
+ return &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: parsedAcl.Owner,
+ },
+ }, err
+ }
+
+ maxkeys, err := utils.ParseUint(maxkeysStr)
+ if err != nil {
+ debuglogger.Logf("error parsing max keys %q: %v",
+ maxkeysStr, err)
+ return &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: parsedAcl.Owner,
+ },
+ }, s3err.GetAPIError(s3err.ErrInvalidMaxKeys)
+ }
+
+ res, err := c.be.ListObjects(ctx.Context(),
+ &s3.ListObjectsInput{
+ Bucket: &bucket,
+ Prefix: &prefix,
+ Marker: &marker,
+ Delimiter: &delimiter,
+ MaxKeys: &maxkeys,
+ })
+ return &Response{
+ Data: res,
+ MetaOpts: &MetaOptions{
+ BucketOwner: parsedAcl.Owner,
+ },
+ }, err
+}
diff --git a/s3api/controllers/bucket-get_test.go b/s3api/controllers/bucket-get_test.go
new file mode 100644
index 00000000..1787863d
--- /dev/null
+++ b/s3api/controllers/bucket-get_test.go
@@ -0,0 +1,1120 @@
+// Copyright 2023 Versity Software
+// This file is licensed under the Apache License, Version 2.0
+// (the "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package controllers
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "testing"
+
+ "github.com/aws/aws-sdk-go-v2/service/s3"
+ "github.com/aws/aws-sdk-go-v2/service/s3/types"
+ "github.com/stretchr/testify/assert"
+ "github.com/versity/versitygw/auth"
+ "github.com/versity/versitygw/s3api/utils"
+ "github.com/versity/versitygw/s3err"
+ "github.com/versity/versitygw/s3response"
+)
+
+func TestS3ApiController_GetBucketTagging(t *testing.T) {
+ tests := []struct {
+ name string
+ input testInput
+ output testOutput
+ }{
+ {
+ name: "verify access fails",
+ input: testInput{
+ locals: accessDeniedLocals,
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ },
+ },
+ err: s3err.GetAPIError(s3err.ErrAccessDenied),
+ },
+ },
+ {
+ name: "backend returns error",
+ input: testInput{
+ locals: defaultLocals,
+ beRes: map[string]string{},
+ beErr: s3err.GetAPIError(s3err.ErrNoSuchBucket),
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ },
+ },
+ err: s3err.GetAPIError(s3err.ErrNoSuchBucket),
+ },
+ },
+ {
+ name: "successful response",
+ input: testInput{
+ locals: defaultLocals,
+ beRes: map[string]string{
+ "key": "val",
+ },
+ },
+ output: testOutput{
+ response: &Response{
+ Data: s3response.Tagging{
+ TagSet: s3response.TagSet{
+ Tags: []s3response.Tag{
+ {Key: "key", Value: "val"},
+ },
+ },
+ },
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ },
+ },
+ },
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ be := &BackendMock{
+ GetBucketTaggingFunc: func(contextMoqParam context.Context, bucket string) (map[string]string, error) {
+ return tt.input.beRes.(map[string]string), tt.input.beErr
+ },
+ GetBucketPolicyFunc: func(contextMoqParam context.Context, bucket string) ([]byte, error) {
+ return nil, s3err.GetAPIError(s3err.ErrAccessDenied)
+ },
+ }
+
+ ctrl := S3ApiController{
+ be: be,
+ }
+
+ testController(
+ t,
+ ctrl.GetBucketTagging,
+ tt.output.response,
+ tt.output.err,
+ ctxInputs{
+ locals: tt.input.locals,
+ body: tt.input.body,
+ })
+ })
+ }
+}
+
+func TestS3ApiController_GetBucketOwnershipControls(t *testing.T) {
+ tests := []struct {
+ name string
+ input testInput
+ output testOutput
+ }{
+ {
+ name: "verify access fails",
+ input: testInput{
+ locals: accessDeniedLocals,
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ },
+ },
+ err: s3err.GetAPIError(s3err.ErrAccessDenied),
+ },
+ },
+ {
+ name: "backend returns error",
+ input: testInput{
+ locals: defaultLocals,
+ beRes: types.ObjectOwnership(""),
+ beErr: s3err.GetAPIError(s3err.ErrNoSuchBucket),
+ },
+ output: testOutput{
+ response: &Response{
+ Data: s3response.OwnershipControls{
+ Rules: []types.OwnershipControlsRule{{}},
+ },
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ },
+ },
+ err: s3err.GetAPIError(s3err.ErrNoSuchBucket),
+ },
+ },
+ {
+ name: "successful response",
+ input: testInput{
+ locals: defaultLocals,
+ beRes: types.ObjectOwnershipBucketOwnerEnforced,
+ },
+ output: testOutput{
+ response: &Response{
+ Data: s3response.OwnershipControls{
+ Rules: []types.OwnershipControlsRule{{ObjectOwnership: types.ObjectOwnershipBucketOwnerEnforced}},
+ },
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ },
+ },
+ },
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ be := &BackendMock{
+ GetBucketOwnershipControlsFunc: func(contextMoqParam context.Context, bucket string) (types.ObjectOwnership, error) {
+ return tt.input.beRes.(types.ObjectOwnership), tt.input.beErr
+ },
+ GetBucketPolicyFunc: func(contextMoqParam context.Context, bucket string) ([]byte, error) {
+ return nil, s3err.GetAPIError(s3err.ErrAccessDenied)
+ },
+ }
+
+ ctrl := S3ApiController{
+ be: be,
+ }
+
+ testController(
+ t,
+ ctrl.GetBucketOwnershipControls,
+ tt.output.response,
+ tt.output.err,
+ ctxInputs{
+ locals: tt.input.locals,
+ body: tt.input.body,
+ })
+ })
+ }
+}
+
+func TestS3ApiController_GetBucketVersioning(t *testing.T) {
+ status := types.BucketVersioningStatusEnabled
+ validRes := s3response.GetBucketVersioningOutput{
+ Status: &status,
+ }
+
+ tests := []struct {
+ name string
+ input testInput
+ output testOutput
+ }{
+ {
+ name: "verify access fails",
+ input: testInput{
+ locals: accessDeniedLocals,
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ },
+ },
+ err: s3err.GetAPIError(s3err.ErrAccessDenied),
+ },
+ },
+ {
+ name: "not admin or root",
+ input: testInput{
+ locals: map[utils.ContextKey]any{
+ utils.ContextKeyIsRoot: false,
+ utils.ContextKeyParsedAcl: auth.ACL{
+ Owner: "root",
+ },
+ utils.ContextKeyAccount: auth.Account{
+ Access: "user",
+ Role: auth.RoleUser,
+ },
+ utils.ContextKeyPublicBucket: true,
+ },
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ },
+ },
+ err: s3err.GetAPIError(s3err.ErrAccessDenied),
+ },
+ },
+ {
+ name: "backend returns error",
+ input: testInput{
+ locals: defaultLocals,
+ beRes: s3response.GetBucketVersioningOutput{},
+ beErr: s3err.GetAPIError(s3err.ErrNoSuchBucket),
+ },
+ output: testOutput{
+ response: &Response{
+ Data: s3response.GetBucketVersioningOutput{},
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ },
+ },
+ err: s3err.GetAPIError(s3err.ErrNoSuchBucket),
+ },
+ },
+ {
+ name: "successful response",
+ input: testInput{
+ locals: defaultLocals,
+ beRes: validRes,
+ },
+ output: testOutput{
+ response: &Response{
+ Data: validRes,
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ },
+ },
+ },
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ be := &BackendMock{
+ GetBucketVersioningFunc: func(contextMoqParam context.Context, bucket string) (s3response.GetBucketVersioningOutput, error) {
+ return tt.input.beRes.(s3response.GetBucketVersioningOutput), tt.input.beErr
+ },
+ GetBucketPolicyFunc: func(contextMoqParam context.Context, bucket string) ([]byte, error) {
+ return nil, s3err.GetAPIError(s3err.ErrAccessDenied)
+ },
+ }
+
+ ctrl := S3ApiController{
+ be: be,
+ }
+
+ testController(
+ t,
+ ctrl.GetBucketVersioning,
+ tt.output.response,
+ tt.output.err,
+ ctxInputs{
+ locals: tt.input.locals,
+ body: tt.input.body,
+ })
+ })
+ }
+}
+
+func TestS3ApiController_GetBucketCors(t *testing.T) {
+ tests := []struct {
+ name string
+ input testInput
+ output testOutput
+ }{
+ {
+ name: "verify access fails",
+ input: testInput{
+ locals: accessDeniedLocals,
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ },
+ },
+ err: s3err.GetAPIError(s3err.ErrAccessDenied),
+ },
+ },
+ {
+ name: "backend returns error",
+ input: testInput{
+ locals: defaultLocals,
+ beRes: []byte{},
+ beErr: s3err.GetAPIError(s3err.ErrNoSuchBucket),
+ },
+ output: testOutput{
+ response: &Response{
+ Data: []byte{},
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ },
+ },
+ err: s3err.GetAPIError(s3err.ErrNoSuchBucket),
+ },
+ },
+ {
+ name: "successful response",
+ input: testInput{
+ locals: defaultLocals,
+ beRes: []byte("mock_cors_resp"),
+ },
+ output: testOutput{
+ response: &Response{
+ Data: []byte("mock_cors_resp"),
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ },
+ },
+ },
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ be := &BackendMock{
+ GetBucketCorsFunc: func(contextMoqParam context.Context, bucket string) ([]byte, error) {
+ return tt.input.beRes.([]byte), tt.input.beErr
+ },
+ GetBucketPolicyFunc: func(contextMoqParam context.Context, bucket string) ([]byte, error) {
+ return nil, s3err.GetAPIError(s3err.ErrAccessDenied)
+ },
+ }
+
+ ctrl := S3ApiController{
+ be: be,
+ }
+
+ testController(
+ t,
+ ctrl.GetBucketCors,
+ tt.output.response,
+ tt.output.err,
+ ctxInputs{
+ locals: tt.input.locals,
+ body: tt.input.body,
+ })
+ })
+ }
+}
+
+func TestS3ApiController_GetBucketPolicy(t *testing.T) {
+ tests := []struct {
+ name string
+ input testInput
+ output testOutput
+ }{
+ {
+ name: "verify access fails",
+ input: testInput{
+ beRes: []byte{},
+ beErr: s3err.GetAPIError(s3err.ErrAccessDenied),
+ locals: accessDeniedLocals,
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ },
+ },
+ err: s3err.GetAPIError(s3err.ErrAccessDenied),
+ },
+ },
+ {
+ name: "backend returns error",
+ input: testInput{
+ locals: defaultLocals,
+ beRes: []byte{},
+ beErr: s3err.GetAPIError(s3err.ErrNoSuchBucket),
+ },
+ output: testOutput{
+ response: &Response{
+ Data: []byte{},
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ },
+ },
+ err: s3err.GetAPIError(s3err.ErrNoSuchBucket),
+ },
+ },
+ {
+ name: "successful response",
+ input: testInput{
+ locals: defaultLocals,
+ beRes: []byte("mock_policy_resp"),
+ },
+ output: testOutput{
+ response: &Response{
+ Data: []byte("mock_policy_resp"),
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ },
+ },
+ },
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ be := &BackendMock{
+ GetBucketPolicyFunc: func(contextMoqParam context.Context, bucket string) ([]byte, error) {
+ return tt.input.beRes.([]byte), tt.input.beErr
+ },
+ }
+
+ ctrl := S3ApiController{
+ be: be,
+ }
+
+ testController(
+ t,
+ ctrl.GetBucketPolicy,
+ tt.output.response,
+ tt.output.err,
+ ctxInputs{
+ locals: tt.input.locals,
+ body: tt.input.body,
+ })
+ })
+ }
+}
+
+func TestS3ApiController_ListObjectVersions(t *testing.T) {
+ listVersionsResult := s3response.ListVersionsResult{
+ Name: utils.GetStringPtr("name"),
+ Prefix: utils.GetStringPtr("prefix"),
+ Delimiter: utils.GetStringPtr("delim"),
+ Versions: []s3response.ObjectVersion{
+ {Key: utils.GetStringPtr("my-key")},
+ },
+ }
+ tests := []struct {
+ name string
+ input testInput
+ output testOutput
+ }{
+ {
+ name: "verify access fails",
+ input: testInput{
+ locals: accessDeniedLocals,
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ },
+ },
+ err: s3err.GetAPIError(s3err.ErrAccessDenied),
+ },
+ },
+ {
+ name: "invalid max keys",
+ input: testInput{
+ locals: defaultLocals,
+ queries: map[string]string{
+ "max-keys": "-1",
+ },
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ },
+ },
+ err: s3err.GetAPIError(s3err.ErrInvalidMaxKeys),
+ },
+ },
+ {
+ name: "backend returns error",
+ input: testInput{
+ locals: defaultLocals,
+ beRes: s3response.ListVersionsResult{},
+ beErr: s3err.GetAPIError(s3err.ErrNoSuchBucket),
+ },
+ output: testOutput{
+ response: &Response{
+ Data: s3response.ListVersionsResult{},
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ },
+ },
+ err: s3err.GetAPIError(s3err.ErrNoSuchBucket),
+ },
+ },
+ {
+ name: "successful response",
+ input: testInput{
+ locals: defaultLocals,
+ beRes: listVersionsResult,
+ },
+ output: testOutput{
+ response: &Response{
+ Data: listVersionsResult,
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ },
+ },
+ },
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ be := &BackendMock{
+ ListObjectVersionsFunc: func(contextMoqParam context.Context, listObjectVersionsInput *s3.ListObjectVersionsInput) (s3response.ListVersionsResult, error) {
+ return tt.input.beRes.(s3response.ListVersionsResult), tt.input.beErr
+ },
+ GetBucketPolicyFunc: func(contextMoqParam context.Context, bucket string) ([]byte, error) {
+ return nil, s3err.GetAPIError(s3err.ErrAccessDenied)
+ },
+ }
+
+ ctrl := S3ApiController{
+ be: be,
+ }
+
+ testController(
+ t,
+ ctrl.ListObjectVersions,
+ tt.output.response,
+ tt.output.err,
+ ctxInputs{
+ locals: tt.input.locals,
+ body: tt.input.body,
+ queries: tt.input.queries,
+ })
+ })
+ }
+}
+
+func TestS3ApiController_GetObjectLockConfiguration(t *testing.T) {
+ cfgBytes, err := json.Marshal(
+ auth.BucketLockConfig{
+ Enabled: true,
+ })
+ assert.NoError(t, err)
+
+ var lockCfg *types.ObjectLockConfiguration
+
+ tests := []struct {
+ name string
+ input testInput
+ output testOutput
+ }{
+ {
+ name: "verify access fails",
+ input: testInput{
+ locals: accessDeniedLocals,
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ },
+ },
+ err: s3err.GetAPIError(s3err.ErrAccessDenied),
+ },
+ },
+ {
+ name: "backend returns error",
+ input: testInput{
+ locals: defaultLocals,
+ beRes: []byte{},
+ beErr: s3err.GetAPIError(s3err.ErrNoSuchBucket),
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ },
+ },
+ err: s3err.GetAPIError(s3err.ErrNoSuchBucket),
+ },
+ },
+ {
+ name: "invalid data from backend",
+ input: testInput{
+ locals: defaultLocals,
+ beRes: []byte{},
+ },
+ output: testOutput{
+ response: &Response{
+ Data: lockCfg,
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ },
+ },
+ err: fmt.Errorf("parse object lock config: "),
+ },
+ },
+ {
+ name: "successful response",
+ input: testInput{
+ locals: defaultLocals,
+ beRes: cfgBytes,
+ },
+ output: testOutput{
+ response: &Response{
+ Data: &types.ObjectLockConfiguration{
+ ObjectLockEnabled: types.ObjectLockEnabledEnabled,
+ Rule: &types.ObjectLockRule{
+ DefaultRetention: nil,
+ },
+ },
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ },
+ },
+ },
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ be := &BackendMock{
+ GetObjectLockConfigurationFunc: func(contextMoqParam context.Context, bucket string) ([]byte, error) {
+ return tt.input.beRes.([]byte), tt.input.beErr
+ },
+ GetBucketPolicyFunc: func(contextMoqParam context.Context, bucket string) ([]byte, error) {
+ return nil, s3err.GetAPIError(s3err.ErrAccessDenied)
+ },
+ }
+
+ ctrl := S3ApiController{
+ be: be,
+ }
+
+ testController(
+ t,
+ ctrl.GetObjectLockConfiguration,
+ tt.output.response,
+ tt.output.err,
+ ctxInputs{
+ locals: tt.input.locals,
+ body: tt.input.body,
+ })
+ })
+ }
+}
+
+func TestS3ApiController_GetBucketAcl(t *testing.T) {
+ aclBytes, err := json.Marshal(
+ auth.ACL{
+ Owner: "root",
+ })
+ assert.NoError(t, err)
+
+ tests := []struct {
+ name string
+ input testInput
+ output testOutput
+ }{
+ {
+ name: "verify access fails",
+ input: testInput{
+ locals: accessDeniedLocals,
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ },
+ },
+ err: s3err.GetAPIError(s3err.ErrAccessDenied),
+ },
+ },
+ {
+ name: "backend returns error",
+ input: testInput{
+ locals: defaultLocals,
+ beRes: []byte{},
+ beErr: s3err.GetAPIError(s3err.ErrNoSuchBucket),
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ },
+ },
+ err: s3err.GetAPIError(s3err.ErrNoSuchBucket),
+ },
+ },
+ {
+ name: "invalid data from backend",
+ input: testInput{
+ locals: defaultLocals,
+ beRes: []byte{'d'},
+ },
+ output: testOutput{
+ response: &Response{
+ Data: auth.GetBucketAclOutput{},
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ },
+ },
+ err: fmt.Errorf("parse acl: "),
+ },
+ },
+ {
+ name: "successful response",
+ input: testInput{
+ locals: defaultLocals,
+ beRes: aclBytes,
+ },
+ output: testOutput{
+ response: &Response{
+ Data: auth.GetBucketAclOutput{
+ Owner: &types.Owner{
+ ID: utils.GetStringPtr("root"),
+ },
+ AccessControlList: auth.AccessControlList{
+ Grants: []auth.Grant{},
+ },
+ },
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ },
+ },
+ },
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ be := &BackendMock{
+ GetBucketAclFunc: func(contextMoqParam context.Context, getBucketAclInput *s3.GetBucketAclInput) ([]byte, error) {
+ return tt.input.beRes.([]byte), tt.input.beErr
+ },
+ GetBucketPolicyFunc: func(contextMoqParam context.Context, bucket string) ([]byte, error) {
+ return nil, s3err.GetAPIError(s3err.ErrAccessDenied)
+ },
+ }
+
+ ctrl := S3ApiController{
+ be: be,
+ }
+
+ testController(
+ t,
+ ctrl.GetBucketAcl,
+ tt.output.response,
+ tt.output.err,
+ ctxInputs{
+ locals: tt.input.locals,
+ body: tt.input.body,
+ })
+ })
+ }
+}
+
+func TestS3ApiController_ListMultipartUploads(t *testing.T) {
+ listMpResult := s3response.ListMultipartUploadsResult{
+ Prefix: "prefix",
+ Delimiter: "delim",
+ Bucket: "bucket",
+ Uploads: []s3response.Upload{
+ {Key: "my-key"},
+ },
+ }
+ tests := []struct {
+ name string
+ input testInput
+ output testOutput
+ }{
+ {
+ name: "verify access fails",
+ input: testInput{
+ locals: accessDeniedLocals,
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ },
+ },
+ err: s3err.GetAPIError(s3err.ErrAccessDenied),
+ },
+ },
+ {
+ name: "invalid max uploads",
+ input: testInput{
+ locals: defaultLocals,
+ queries: map[string]string{
+ "max-uploads": "-1",
+ },
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ },
+ },
+ err: s3err.GetAPIError(s3err.ErrInvalidMaxUploads),
+ },
+ },
+ {
+ name: "backend returns error",
+ input: testInput{
+ locals: defaultLocals,
+ beRes: s3response.ListMultipartUploadsResult{},
+ beErr: s3err.GetAPIError(s3err.ErrNoSuchBucket),
+ },
+ output: testOutput{
+ response: &Response{
+ Data: s3response.ListMultipartUploadsResult{},
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ },
+ },
+ err: s3err.GetAPIError(s3err.ErrNoSuchBucket),
+ },
+ },
+ {
+ name: "successful response",
+ input: testInput{
+ locals: defaultLocals,
+ beRes: listMpResult,
+ },
+ output: testOutput{
+ response: &Response{
+ Data: listMpResult,
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ },
+ },
+ },
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ be := &BackendMock{
+ ListMultipartUploadsFunc: func(contextMoqParam context.Context, listMultipartUploadsInput *s3.ListMultipartUploadsInput) (s3response.ListMultipartUploadsResult, error) {
+ return tt.input.beRes.(s3response.ListMultipartUploadsResult), tt.input.beErr
+ },
+ GetBucketPolicyFunc: func(contextMoqParam context.Context, bucket string) ([]byte, error) {
+ return nil, s3err.GetAPIError(s3err.ErrAccessDenied)
+ },
+ }
+
+ ctrl := S3ApiController{
+ be: be,
+ }
+
+ testController(
+ t,
+ ctrl.ListMultipartUploads,
+ tt.output.response,
+ tt.output.err,
+ ctxInputs{
+ locals: tt.input.locals,
+ body: tt.input.body,
+ queries: tt.input.queries,
+ })
+ })
+ }
+}
+
+func TestS3ApiController_ListObjectsV2(t *testing.T) {
+ listV2Result := s3response.ListObjectsV2Result{
+ Name: utils.GetStringPtr("name"),
+ Prefix: utils.GetStringPtr("prefix"),
+ Delimiter: utils.GetStringPtr("delim"),
+ Contents: []s3response.Object{
+ {Key: utils.GetStringPtr("my-key")},
+ },
+ }
+ tests := []struct {
+ name string
+ input testInput
+ output testOutput
+ }{
+ {
+ name: "verify access fails",
+ input: testInput{
+ locals: accessDeniedLocals,
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ },
+ },
+ err: s3err.GetAPIError(s3err.ErrAccessDenied),
+ },
+ },
+ {
+ name: "invalid max keys",
+ input: testInput{
+ locals: defaultLocals,
+ queries: map[string]string{
+ "max-keys": "-1",
+ },
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ },
+ },
+ err: s3err.GetAPIError(s3err.ErrInvalidMaxKeys),
+ },
+ },
+ {
+ name: "backend returns error",
+ input: testInput{
+ locals: defaultLocals,
+ beRes: s3response.ListObjectsV2Result{},
+ beErr: s3err.GetAPIError(s3err.ErrNoSuchBucket),
+ },
+ output: testOutput{
+ response: &Response{
+ Data: s3response.ListObjectsV2Result{},
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ },
+ },
+ err: s3err.GetAPIError(s3err.ErrNoSuchBucket),
+ },
+ },
+ {
+ name: "successful response",
+ input: testInput{
+ locals: defaultLocals,
+ beRes: listV2Result,
+ },
+ output: testOutput{
+ response: &Response{
+ Data: listV2Result,
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ },
+ },
+ },
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ be := &BackendMock{
+ ListObjectsV2Func: func(contextMoqParam context.Context, listObjectsV2Input *s3.ListObjectsV2Input) (s3response.ListObjectsV2Result, error) {
+ return tt.input.beRes.(s3response.ListObjectsV2Result), tt.input.beErr
+ },
+ GetBucketPolicyFunc: func(contextMoqParam context.Context, bucket string) ([]byte, error) {
+ return nil, s3err.GetAPIError(s3err.ErrAccessDenied)
+ },
+ }
+
+ ctrl := S3ApiController{
+ be: be,
+ }
+
+ testController(
+ t,
+ ctrl.ListObjectsV2,
+ tt.output.response,
+ tt.output.err,
+ ctxInputs{
+ locals: tt.input.locals,
+ body: tt.input.body,
+ queries: tt.input.queries,
+ })
+ })
+ }
+}
+
+func TestS3ApiController_ListObjects(t *testing.T) {
+ listResult := s3response.ListObjectsResult{
+ Name: utils.GetStringPtr("name"),
+ Prefix: utils.GetStringPtr("prefix"),
+ Delimiter: utils.GetStringPtr("delim"),
+ Contents: []s3response.Object{
+ {Key: utils.GetStringPtr("my-key")},
+ },
+ }
+ tests := []struct {
+ name string
+ input testInput
+ output testOutput
+ }{
+ {
+ name: "verify access fails",
+ input: testInput{
+ locals: accessDeniedLocals,
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ },
+ },
+ err: s3err.GetAPIError(s3err.ErrAccessDenied),
+ },
+ },
+ {
+ name: "invalid max keys",
+ input: testInput{
+ locals: defaultLocals,
+ queries: map[string]string{
+ "max-keys": "-1",
+ },
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ },
+ },
+ err: s3err.GetAPIError(s3err.ErrInvalidMaxKeys),
+ },
+ },
+ {
+ name: "backend returns error",
+ input: testInput{
+ locals: defaultLocals,
+ beRes: s3response.ListObjectsResult{},
+ beErr: s3err.GetAPIError(s3err.ErrNoSuchBucket),
+ },
+ output: testOutput{
+ response: &Response{
+ Data: s3response.ListObjectsResult{},
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ },
+ },
+ err: s3err.GetAPIError(s3err.ErrNoSuchBucket),
+ },
+ },
+ {
+ name: "successful response",
+ input: testInput{
+ locals: defaultLocals,
+ beRes: listResult,
+ },
+ output: testOutput{
+ response: &Response{
+ Data: listResult,
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ },
+ },
+ },
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ be := &BackendMock{
+ ListObjectsFunc: func(contextMoqParam context.Context, listObjectsInput *s3.ListObjectsInput) (s3response.ListObjectsResult, error) {
+ return tt.input.beRes.(s3response.ListObjectsResult), tt.input.beErr
+ },
+ GetBucketPolicyFunc: func(contextMoqParam context.Context, bucket string) ([]byte, error) {
+ return nil, s3err.GetAPIError(s3err.ErrAccessDenied)
+ },
+ }
+
+ ctrl := S3ApiController{
+ be: be,
+ }
+
+ testController(
+ t,
+ ctrl.ListObjects,
+ tt.output.response,
+ tt.output.err,
+ ctxInputs{
+ locals: tt.input.locals,
+ body: tt.input.body,
+ queries: tt.input.queries,
+ })
+ })
+ }
+}
diff --git a/s3api/controllers/bucket-head.go b/s3api/controllers/bucket-head.go
new file mode 100644
index 00000000..f4f063dc
--- /dev/null
+++ b/s3api/controllers/bucket-head.go
@@ -0,0 +1,73 @@
+// Copyright 2023 Versity Software
+// This file is licensed under the Apache License, Version 2.0
+// (the "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package controllers
+
+import (
+ "github.com/aws/aws-sdk-go-v2/service/s3"
+ "github.com/gofiber/fiber/v2"
+ "github.com/versity/versitygw/auth"
+ "github.com/versity/versitygw/s3api/utils"
+)
+
+func (c S3ApiController) HeadBucket(ctx *fiber.Ctx) (*Response, error) {
+ bucket := ctx.Params("bucket")
+ acct := utils.ContextKeyAccount.Get(ctx).(auth.Account)
+ isRoot := utils.ContextKeyIsRoot.Get(ctx).(bool)
+ region := utils.ContextKeyRegion.Get(ctx).(string)
+ parsedAcl := utils.ContextKeyParsedAcl.Get(ctx).(auth.ACL)
+ 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,
+ Action: auth.ListBucketAction,
+ IsBucketPublic: isPublicBucket,
+ })
+ if err != nil {
+ return &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: parsedAcl.Owner,
+ },
+ }, err
+ }
+
+ _, err = c.be.HeadBucket(ctx.Context(),
+ &s3.HeadBucketInput{
+ Bucket: &bucket,
+ })
+
+ if err != nil {
+ return &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: parsedAcl.Owner,
+ },
+ }, err
+ }
+
+ return &Response{
+ Headers: map[string]*string{
+ "X-Amz-Access-Point-Alias": utils.GetStringPtr("false"),
+ "X-Amz-Bucket-Region": utils.GetStringPtr(region),
+ },
+ MetaOpts: &MetaOptions{
+ BucketOwner: parsedAcl.Owner,
+ },
+ }, nil
+}
diff --git a/s3api/controllers/bucket-head_test.go b/s3api/controllers/bucket-head_test.go
new file mode 100644
index 00000000..4b5a6e4d
--- /dev/null
+++ b/s3api/controllers/bucket-head_test.go
@@ -0,0 +1,136 @@
+// Copyright 2023 Versity Software
+// This file is licensed under the Apache License, Version 2.0
+// (the "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package controllers
+
+import (
+ "context"
+ "testing"
+
+ "github.com/aws/aws-sdk-go-v2/service/s3"
+ "github.com/versity/versitygw/auth"
+ "github.com/versity/versitygw/s3api/utils"
+ "github.com/versity/versitygw/s3err"
+)
+
+func TestS3ApiController_HeadBucket(t *testing.T) {
+ region := "us-east-1"
+ tests := []struct {
+ name string
+ input testInput
+ output testOutput
+ }{
+ {
+ name: "verify access fails",
+ input: testInput{
+ locals: map[utils.ContextKey]any{
+ utils.ContextKeyIsRoot: false,
+ utils.ContextKeyParsedAcl: auth.ACL{
+ Owner: "root",
+ },
+ utils.ContextKeyAccount: auth.Account{
+ Access: "user",
+ Role: auth.RoleUser,
+ },
+ utils.ContextKeyRegion: region,
+ },
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ },
+ },
+ err: s3err.GetAPIError(s3err.ErrAccessDenied),
+ },
+ },
+ {
+ name: "backend returns error",
+ input: testInput{
+ locals: map[utils.ContextKey]any{
+ utils.ContextKeyIsRoot: true,
+ utils.ContextKeyParsedAcl: auth.ACL{
+ Owner: "root",
+ },
+ utils.ContextKeyAccount: auth.Account{
+ Access: "root",
+ Role: auth.RoleAdmin,
+ },
+ utils.ContextKeyRegion: region,
+ },
+ beErr: s3err.GetAPIError(s3err.ErrInvalidAccessKeyID),
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ },
+ },
+ err: s3err.GetAPIError(s3err.ErrInvalidAccessKeyID),
+ },
+ },
+ {
+ name: "successful response",
+ input: testInput{
+ locals: map[utils.ContextKey]any{
+ utils.ContextKeyIsRoot: true,
+ utils.ContextKeyParsedAcl: auth.ACL{
+ Owner: "root",
+ },
+ utils.ContextKeyAccount: auth.Account{
+ Access: "root",
+ Role: auth.RoleAdmin,
+ },
+ utils.ContextKeyRegion: region,
+ },
+ },
+ output: testOutput{
+ response: &Response{
+ Headers: map[string]*string{
+ "X-Amz-Access-Point-Alias": utils.GetStringPtr("false"),
+ "X-Amz-Bucket-Region": utils.GetStringPtr(region),
+ },
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ },
+ },
+ },
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ be := &BackendMock{
+ HeadBucketFunc: func(contextMoqParam context.Context, headBucketInput *s3.HeadBucketInput) (*s3.HeadBucketOutput, error) {
+ return &s3.HeadBucketOutput{}, tt.input.beErr
+ },
+ GetBucketPolicyFunc: func(contextMoqParam context.Context, bucket string) ([]byte, error) {
+ return nil, s3err.GetAPIError(s3err.ErrAccessDenied)
+ },
+ }
+
+ ctrl := S3ApiController{
+ be: be,
+ }
+
+ testController(
+ t,
+ ctrl.HeadBucket,
+ tt.output.response,
+ tt.output.err,
+ ctxInputs{
+ locals: tt.input.locals,
+ })
+ })
+ }
+}
diff --git a/s3api/controllers/bucket-list.go b/s3api/controllers/bucket-list.go
new file mode 100644
index 00000000..5cae8939
--- /dev/null
+++ b/s3api/controllers/bucket-list.go
@@ -0,0 +1,58 @@
+// Copyright 2023 Versity Software
+// This file is licensed under the Apache License, Version 2.0
+// (the "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package controllers
+
+import (
+ "strconv"
+
+ "github.com/gofiber/fiber/v2"
+ "github.com/versity/versitygw/auth"
+ "github.com/versity/versitygw/s3api/debuglogger"
+ "github.com/versity/versitygw/s3api/utils"
+ "github.com/versity/versitygw/s3err"
+ "github.com/versity/versitygw/s3response"
+)
+
+func (c S3ApiController) ListBuckets(ctx *fiber.Ctx) (*Response, error) {
+ cToken := ctx.Query("continuation-token")
+ prefix := ctx.Query("prefix")
+ maxBucketsStr := ctx.Query("max-buckets")
+ acct := utils.ContextKeyAccount.Get(ctx).(auth.Account)
+
+ maxBuckets := defaultMaxBuckets
+ if maxBucketsStr != "" {
+ maxBucketsParsed, err := strconv.ParseInt(maxBucketsStr, 10, 32)
+ if err != nil || maxBucketsParsed < 0 || maxBucketsParsed > int64(defaultMaxBuckets) {
+ debuglogger.Logf("error parsing max-buckets %q: %v", maxBucketsStr, err)
+ return &Response{
+ MetaOpts: &MetaOptions{},
+ }, s3err.GetAPIError(s3err.ErrInvalidMaxBuckets)
+ }
+ maxBuckets = int32(maxBucketsParsed)
+ }
+
+ res, err := c.be.ListBuckets(ctx.Context(),
+ s3response.ListBucketsInput{
+ Owner: acct.Access,
+ IsAdmin: acct.Role == auth.RoleAdmin,
+ MaxBuckets: maxBuckets,
+ ContinuationToken: cToken,
+ Prefix: prefix,
+ })
+ return &Response{
+ Data: res,
+ MetaOpts: &MetaOptions{},
+ }, err
+}
diff --git a/s3api/controllers/bucket-list_test.go b/s3api/controllers/bucket-list_test.go
new file mode 100644
index 00000000..7f6bcc79
--- /dev/null
+++ b/s3api/controllers/bucket-list_test.go
@@ -0,0 +1,112 @@
+// Copyright 2023 Versity Software
+// This file is licensed under the Apache License, Version 2.0
+// (the "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package controllers
+
+import (
+ "context"
+ "testing"
+
+ "github.com/versity/versitygw/s3err"
+ "github.com/versity/versitygw/s3response"
+)
+
+func TestS3ApiController_ListBuckets(t *testing.T) {
+ validRes := s3response.ListAllMyBucketsResult{
+ Owner: s3response.CanonicalUser{
+ ID: "root",
+ },
+ Buckets: s3response.ListAllMyBucketsList{
+ Bucket: []s3response.ListAllMyBucketsEntry{
+ {Name: "test"},
+ },
+ },
+ }
+
+ tests := []struct {
+ name string
+ input testInput
+ output testOutput
+ }{
+ {
+ name: "invalid max buckets",
+ input: testInput{
+ locals: defaultLocals,
+ queries: map[string]string{
+ "max-buckets": "-1",
+ },
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{},
+ },
+ err: s3err.GetAPIError(s3err.ErrInvalidMaxBuckets),
+ },
+ },
+ {
+ name: "backend returns error",
+ input: testInput{
+ locals: defaultLocals,
+ beRes: validRes,
+ beErr: s3err.GetAPIError(s3err.ErrNoSuchBucket),
+ },
+ output: testOutput{
+ response: &Response{
+ Data: validRes,
+ MetaOpts: &MetaOptions{},
+ },
+ err: s3err.GetAPIError(s3err.ErrNoSuchBucket),
+ },
+ },
+ {
+ name: "successful response",
+ input: testInput{
+ locals: defaultLocals,
+ beRes: validRes,
+ queries: map[string]string{
+ "max-buckets": "3",
+ },
+ },
+ output: testOutput{
+ response: &Response{
+ Data: validRes,
+ MetaOpts: &MetaOptions{},
+ },
+ },
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ be := &BackendMock{
+ ListBucketsFunc: func(contextMoqParam context.Context, listBucketsInput s3response.ListBucketsInput) (s3response.ListAllMyBucketsResult, error) {
+ return tt.input.beRes.(s3response.ListAllMyBucketsResult), tt.input.beErr
+ },
+ }
+
+ ctrl := S3ApiController{
+ be: be,
+ }
+
+ testController(
+ t,
+ ctrl.ListBuckets,
+ tt.output.response,
+ tt.output.err,
+ ctxInputs{
+ locals: tt.input.locals,
+ queries: tt.input.queries,
+ })
+ })
+ }
+}
diff --git a/s3api/controllers/bucket-post.go b/s3api/controllers/bucket-post.go
new file mode 100644
index 00000000..22e5f2f0
--- /dev/null
+++ b/s3api/controllers/bucket-post.go
@@ -0,0 +1,94 @@
+// Copyright 2023 Versity Software
+// This file is licensed under the Apache License, Version 2.0
+// (the "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package controllers
+
+import (
+ "encoding/xml"
+ "strings"
+
+ "github.com/aws/aws-sdk-go-v2/service/s3"
+ "github.com/aws/aws-sdk-go-v2/service/s3/types"
+ "github.com/gofiber/fiber/v2"
+ "github.com/versity/versitygw/auth"
+ "github.com/versity/versitygw/s3api/debuglogger"
+ "github.com/versity/versitygw/s3api/utils"
+ "github.com/versity/versitygw/s3err"
+ "github.com/versity/versitygw/s3event"
+ "github.com/versity/versitygw/s3response"
+)
+
+func (c S3ApiController) DeleteObjects(ctx *fiber.Ctx) (*Response, error) {
+ bucket := ctx.Params("bucket")
+ bypass := strings.EqualFold(ctx.Get("X-Amz-Bypass-Governance-Retention"), "true")
+ acct := utils.ContextKeyAccount.Get(ctx).(auth.Account)
+ isRoot := utils.ContextKeyIsRoot.Get(ctx).(bool)
+ parsedAcl := utils.ContextKeyParsedAcl.Get(ctx).(auth.ACL)
+ IsBucketPublic := 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.DeleteObjectAction,
+ IsBucketPublic: IsBucketPublic,
+ })
+ if err != nil {
+ return &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: parsedAcl.Owner,
+ },
+ }, err
+ }
+
+ var dObj s3response.DeleteObjects
+ err = xml.Unmarshal(ctx.Body(), &dObj)
+ if err != nil {
+ debuglogger.Logf("error unmarshalling delete objects: %v", err)
+ return &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: parsedAcl.Owner,
+ },
+ }, s3err.GetAPIError(s3err.ErrInvalidRequest)
+ }
+
+ err = auth.CheckObjectAccess(ctx.Context(), bucket, acct.Access, dObj.Objects, bypass, IsBucketPublic, c.be)
+ if err != nil {
+ return &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: parsedAcl.Owner,
+ },
+ }, err
+ }
+
+ res, err := c.be.DeleteObjects(ctx.Context(),
+ &s3.DeleteObjectsInput{
+ Bucket: &bucket,
+ Delete: &types.Delete{
+ Objects: dObj.Objects,
+ },
+ })
+ return &Response{
+ Data: res,
+ MetaOpts: &MetaOptions{
+ ObjectCount: int64(len(dObj.Objects)),
+ BucketOwner: parsedAcl.Owner,
+ EventName: s3event.EventObjectRemovedDeleteObjects,
+ },
+ }, err
+}
diff --git a/s3api/controllers/bucket-post_test.go b/s3api/controllers/bucket-post_test.go
new file mode 100644
index 00000000..40cf2879
--- /dev/null
+++ b/s3api/controllers/bucket-post_test.go
@@ -0,0 +1,165 @@
+// Copyright 2023 Versity Software
+// This file is licensed under the Apache License, Version 2.0
+// (the "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package controllers
+
+import (
+ "context"
+ "encoding/xml"
+ "testing"
+
+ "github.com/aws/aws-sdk-go-v2/service/s3"
+ "github.com/aws/aws-sdk-go-v2/service/s3/types"
+ "github.com/stretchr/testify/assert"
+ "github.com/versity/versitygw/s3api/utils"
+ "github.com/versity/versitygw/s3err"
+ "github.com/versity/versitygw/s3event"
+ "github.com/versity/versitygw/s3response"
+)
+
+func TestS3ApiController_DeleteObjects(t *testing.T) {
+ validBody, err := xml.Marshal(s3response.DeleteObjects{
+ Objects: []types.ObjectIdentifier{
+ {Key: utils.GetStringPtr("obj")},
+ },
+ })
+ assert.NoError(t, err)
+
+ validRes := s3response.DeleteResult{
+ Deleted: []types.DeletedObject{
+ {Key: utils.GetStringPtr("key")},
+ },
+ }
+
+ tests := []struct {
+ name string
+ input testInput
+ output testOutput
+ }{
+ {
+ name: "verify access fails",
+ input: testInput{
+ locals: accessDeniedLocals,
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ },
+ },
+ err: s3err.GetAPIError(s3err.ErrAccessDenied),
+ },
+ },
+ {
+ name: "invalid request body",
+ input: testInput{
+ locals: defaultLocals,
+ body: []byte("invalid_body"),
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ },
+ },
+ err: s3err.GetAPIError(s3err.ErrInvalidRequest),
+ },
+ },
+ {
+ name: "check object access returns error",
+ input: testInput{
+ locals: defaultLocals,
+ body: validBody,
+ extraMockErr: s3err.GetAPIError(s3err.ErrObjectLocked),
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ },
+ },
+ err: s3err.GetAPIError(s3err.ErrObjectLocked),
+ },
+ },
+ {
+ name: "backend returns error",
+ input: testInput{
+ locals: defaultLocals,
+ beRes: s3response.DeleteResult{},
+ beErr: s3err.GetAPIError(s3err.ErrNoSuchBucket),
+ body: validBody,
+ extraMockErr: s3err.GetAPIError(s3err.ErrObjectLockConfigurationNotFound),
+ },
+ output: testOutput{
+ response: &Response{
+ Data: s3response.DeleteResult{},
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ EventName: s3event.EventObjectRemovedDeleteObjects,
+ ObjectCount: 1,
+ },
+ },
+ err: s3err.GetAPIError(s3err.ErrNoSuchBucket),
+ },
+ },
+ {
+ name: "successful response",
+ input: testInput{
+ locals: defaultLocals,
+ body: validBody,
+ beRes: validRes,
+ extraMockErr: s3err.GetAPIError(s3err.ErrObjectLockConfigurationNotFound),
+ },
+ output: testOutput{
+ response: &Response{
+ Data: validRes,
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ EventName: s3event.EventObjectRemovedDeleteObjects,
+ ObjectCount: 1,
+ },
+ },
+ },
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ be := &BackendMock{
+ DeleteObjectsFunc: func(contextMoqParam context.Context, deleteObjectsInput *s3.DeleteObjectsInput) (s3response.DeleteResult, error) {
+ return tt.input.beRes.(s3response.DeleteResult), tt.input.beErr
+ },
+ GetBucketPolicyFunc: func(contextMoqParam context.Context, bucket string) ([]byte, error) {
+ return nil, s3err.GetAPIError(s3err.ErrAccessDenied)
+ },
+ GetObjectLockConfigurationFunc: func(contextMoqParam context.Context, bucket string) ([]byte, error) {
+ return nil, tt.input.extraMockErr
+ },
+ }
+
+ ctrl := S3ApiController{
+ be: be,
+ }
+
+ testController(
+ t,
+ ctrl.DeleteObjects,
+ tt.output.response,
+ tt.output.err,
+ ctxInputs{
+ locals: tt.input.locals,
+ body: tt.input.body,
+ })
+ })
+ }
+}
diff --git a/s3api/controllers/bucket-put.go b/s3api/controllers/bucket-put.go
new file mode 100644
index 00000000..7fbd0ea3
--- /dev/null
+++ b/s3api/controllers/bucket-put.go
@@ -0,0 +1,548 @@
+// Copyright 2023 Versity Software
+// This file is licensed under the Apache License, Version 2.0
+// (the "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package controllers
+
+import (
+ "encoding/xml"
+ "errors"
+ "fmt"
+ "net/http"
+ "strings"
+
+ "github.com/aws/aws-sdk-go-v2/service/s3"
+ "github.com/aws/aws-sdk-go-v2/service/s3/types"
+ "github.com/gofiber/fiber/v2"
+ "github.com/versity/versitygw/auth"
+ "github.com/versity/versitygw/s3api/debuglogger"
+ "github.com/versity/versitygw/s3api/utils"
+ "github.com/versity/versitygw/s3err"
+ "github.com/versity/versitygw/s3response"
+)
+
+func (c S3ApiController) PutBucketTagging(ctx *fiber.Ctx) (*Response, error) {
+ bucket := ctx.Params("bucket")
+ parsedAcl := utils.ContextKeyParsedAcl.Get(ctx).(auth.ACL)
+ acct := utils.ContextKeyAccount.Get(ctx).(auth.Account)
+ isRoot := utils.ContextKeyIsRoot.Get(ctx).(bool)
+ 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,
+ })
+ if err != nil {
+ return &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: parsedAcl.Owner,
+ },
+ }, err
+ }
+
+ tagging, err := utils.ParseTagging(ctx.Body(), utils.TagLimitBucket)
+ if err != nil {
+ return &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: parsedAcl.Owner,
+ },
+ }, err
+ }
+
+ err = c.be.PutBucketTagging(ctx.Context(), bucket, tagging)
+ return &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: parsedAcl.Owner,
+ Status: http.StatusNoContent,
+ },
+ }, err
+}
+
+func (c S3ApiController) PutBucketOwnershipControls(ctx *fiber.Ctx) (*Response, error) {
+ bucket := ctx.Params("bucket")
+ parsedAcl := utils.ContextKeyParsedAcl.Get(ctx).(auth.ACL)
+ acct := utils.ContextKeyAccount.Get(ctx).(auth.Account)
+ isRoot := utils.ContextKeyIsRoot.Get(ctx).(bool)
+
+ 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.PutBucketOwnershipControlsAction,
+ }); err != nil {
+ return &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: parsedAcl.Owner,
+ },
+ }, err
+ }
+
+ var ownershipControls s3response.OwnershipControls
+ if err := xml.Unmarshal(ctx.Body(), &ownershipControls); err != nil {
+ debuglogger.Logf("failed to unmarshal request body: %v", err)
+ return &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: parsedAcl.Owner,
+ },
+ }, s3err.GetAPIError(s3err.ErrMalformedXML)
+ }
+
+ rulesCount := len(ownershipControls.Rules)
+ isValidOwnership := utils.IsValidOwnership(ownershipControls.Rules[0].ObjectOwnership)
+ if rulesCount != 1 || !isValidOwnership {
+ if rulesCount != 1 {
+ debuglogger.Logf("ownership control rules should be 1, got %v", rulesCount)
+ }
+ return &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: parsedAcl.Owner,
+ },
+ }, s3err.GetAPIError(s3err.ErrMalformedXML)
+ }
+
+ err := c.be.PutBucketOwnershipControls(ctx.Context(), bucket, ownershipControls.Rules[0].ObjectOwnership)
+ return &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: parsedAcl.Owner,
+ },
+ }, err
+}
+
+func (c S3ApiController) PutBucketVersioning(ctx *fiber.Ctx) (*Response, error) {
+ bucket := ctx.Params("bucket")
+ parsedAcl := utils.ContextKeyParsedAcl.Get(ctx).(auth.ACL)
+ acct := utils.ContextKeyAccount.Get(ctx).(auth.Account)
+ isRoot := utils.ContextKeyIsRoot.Get(ctx).(bool)
+ 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,
+ })
+ if err != nil {
+ return &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: parsedAcl.Owner,
+ },
+ }, err
+ }
+
+ var versioningConf types.VersioningConfiguration
+ err = xml.Unmarshal(ctx.Body(), &versioningConf)
+ if err != nil {
+ debuglogger.Logf("error unmarshalling versioning configuration: %v", err)
+ return &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: parsedAcl.Owner,
+ },
+ }, s3err.GetAPIError(s3err.ErrInvalidRequest)
+ }
+
+ if versioningConf.Status != types.BucketVersioningStatusEnabled &&
+ versioningConf.Status != types.BucketVersioningStatusSuspended {
+ debuglogger.Logf("invalid versioning configuration status: %v", versioningConf.Status)
+ return &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: parsedAcl.Owner,
+ },
+ }, s3err.GetAPIError(s3err.ErrMalformedXML)
+ }
+
+ err = c.be.PutBucketVersioning(ctx.Context(), bucket, versioningConf.Status)
+ return &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: parsedAcl.Owner,
+ },
+ }, err
+}
+
+func (c S3ApiController) PutObjectLockConfiguration(ctx *fiber.Ctx) (*Response, error) {
+ bucket := ctx.Params("bucket")
+ parsedAcl := utils.ContextKeyParsedAcl.Get(ctx).(auth.ACL)
+ acct := utils.ContextKeyAccount.Get(ctx).(auth.Account)
+ isRoot := utils.ContextKeyIsRoot.Get(ctx).(bool)
+ 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,
+ }); err != nil {
+ return &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: parsedAcl.Owner,
+ },
+ }, err
+ }
+
+ config, err := auth.ParseBucketLockConfigurationInput(ctx.Body())
+ if err != nil {
+ return &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: parsedAcl.Owner,
+ },
+ }, err
+ }
+
+ err = c.be.PutObjectLockConfiguration(ctx.Context(), bucket, config)
+ return &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: parsedAcl.Owner,
+ },
+ }, err
+}
+
+func (c S3ApiController) PutBucketCors(ctx *fiber.Ctx) (*Response, error) {
+ bucket := ctx.Params("bucket")
+ parsedAcl := utils.ContextKeyParsedAcl.Get(ctx).(auth.ACL)
+ acct := utils.ContextKeyAccount.Get(ctx).(auth.Account)
+ isRoot := utils.ContextKeyIsRoot.Get(ctx).(bool)
+ 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,
+ })
+ if err != nil {
+ return &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: parsedAcl.Owner,
+ },
+ }, err
+ }
+
+ err = c.be.PutBucketCors(ctx.Context(), []byte{})
+ return &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: parsedAcl.Owner,
+ },
+ }, err
+}
+
+func (c S3ApiController) PutBucketPolicy(ctx *fiber.Ctx) (*Response, error) {
+ bucket := ctx.Params("bucket")
+ parsedAcl := utils.ContextKeyParsedAcl.Get(ctx).(auth.ACL)
+ acct := utils.ContextKeyAccount.Get(ctx).(auth.Account)
+ isRoot := utils.ContextKeyIsRoot.Get(ctx).(bool)
+
+ 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.PutBucketPolicyAction,
+ })
+ if err != nil {
+ return &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: parsedAcl.Owner,
+ },
+ }, err
+ }
+
+ err = auth.ValidatePolicyDocument(ctx.Body(), bucket, c.iam)
+ if err != nil {
+ return &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: parsedAcl.Owner,
+ },
+ }, err
+ }
+
+ err = c.be.PutBucketPolicy(ctx.Context(), bucket, ctx.Body())
+ return &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: parsedAcl.Owner,
+ },
+ }, err
+}
+
+func (c S3ApiController) PutBucketAcl(ctx *fiber.Ctx) (*Response, error) {
+ bucket := ctx.Params("bucket")
+ acl := ctx.Get("X-Amz-Acl")
+ grantFullControl := ctx.Get("X-Amz-Grant-Full-Control")
+ grantRead := ctx.Get("X-Amz-Grant-Read")
+ grantReadACP := ctx.Get("X-Amz-Grant-Read-Acp")
+ grantWrite := ctx.Get("X-Amz-Grant-Write")
+ grantWriteACP := ctx.Get("X-Amz-Grant-Write-Acp")
+ // context locals
+ parsedAcl := utils.ContextKeyParsedAcl.Get(ctx).(auth.ACL)
+ acct := utils.ContextKeyAccount.Get(ctx).(auth.Account)
+ isRoot := utils.ContextKeyIsRoot.Get(ctx).(bool)
+
+ grants := grantFullControl + grantRead + grantReadACP + grantWrite + grantWriteACP
+ var input *auth.PutBucketAclInput
+
+ err := auth.VerifyAccess(ctx.Context(), c.be,
+ auth.AccessOptions{
+ Readonly: c.readonly,
+ Acl: parsedAcl,
+ AclPermission: auth.PermissionWriteAcp,
+ IsRoot: isRoot,
+ Acc: acct,
+ Bucket: bucket,
+ Action: auth.PutBucketAclAction,
+ })
+ if err != nil {
+ return &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: parsedAcl.Owner,
+ },
+ }, err
+ }
+
+ ownership, err := c.be.GetBucketOwnershipControls(ctx.Context(), bucket)
+ if err != nil && !errors.Is(err, s3err.GetAPIError(s3err.ErrOwnershipControlsNotFound)) {
+ return &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: parsedAcl.Owner,
+ },
+ }, err
+ }
+ if ownership == types.ObjectOwnershipBucketOwnerEnforced {
+ debuglogger.Logf("bucket acls are disabled")
+ return &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: parsedAcl.Owner,
+ },
+ }, s3err.GetAPIError(s3err.ErrAclNotSupported)
+ }
+
+ if len(ctx.Body()) > 0 {
+ var accessControlPolicy auth.AccessControlPolicy
+ err := xml.Unmarshal(ctx.Body(), &accessControlPolicy)
+ if err != nil {
+ debuglogger.Logf("error unmarshalling access control policy: %v", err)
+ return &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: parsedAcl.Owner,
+ },
+ }, s3err.GetAPIError(s3err.ErrMalformedACL)
+ }
+
+ err = accessControlPolicy.Validate()
+ if err != nil {
+ debuglogger.Logf("invalid access control policy: %v", err)
+ return &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: parsedAcl.Owner,
+ },
+ }, err
+ }
+
+ if *accessControlPolicy.Owner.ID != parsedAcl.Owner {
+ debuglogger.Logf("invalid access control policy owner id: %v, expected %v", *accessControlPolicy.Owner.ID, parsedAcl.Owner)
+ return &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: parsedAcl.Owner,
+ },
+ }, s3err.APIError{
+ Code: "InvalidArgument",
+ Description: "Invalid id",
+ HTTPStatusCode: http.StatusBadRequest,
+ }
+ }
+
+ if grants+acl != "" {
+ debuglogger.Logf("invalid request: %q (grants) %q (acl)",
+ grants, acl)
+ return &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: parsedAcl.Owner,
+ },
+ }, s3err.GetAPIError(s3err.ErrUnexpectedContent)
+ }
+
+ input = &auth.PutBucketAclInput{
+ Bucket: &bucket,
+ AccessControlPolicy: &accessControlPolicy,
+ }
+ } else if acl != "" {
+ if acl != "private" && acl != "public-read" && acl != "public-read-write" {
+ debuglogger.Logf("invalid acl: %q", acl)
+ return &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: parsedAcl.Owner,
+ },
+ }, s3err.GetAPIError(s3err.ErrInvalidRequest)
+ }
+ if grants != "" {
+ debuglogger.Logf("invalid request: %q (grants) %q (acl)",
+ grants, acl)
+ return &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: parsedAcl.Owner,
+ },
+ }, s3err.GetAPIError(s3err.ErrBothCannedAndHeaderGrants)
+ }
+
+ input = &auth.PutBucketAclInput{
+ Bucket: &bucket,
+ ACL: types.BucketCannedACL(acl),
+ }
+ } else if grants != "" {
+ input = &auth.PutBucketAclInput{
+ Bucket: &bucket,
+ GrantFullControl: &grantFullControl,
+ GrantRead: &grantRead,
+ GrantReadACP: &grantReadACP,
+ GrantWrite: &grantWrite,
+ GrantWriteACP: &grantWriteACP,
+ }
+ } else {
+ debuglogger.Logf("none of the bucket acl options has been specified: canned, req headers, req body")
+ return &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: parsedAcl.Owner,
+ },
+ }, s3err.GetAPIError(s3err.ErrMissingSecurityHeader)
+ }
+
+ updAcl, err := auth.UpdateACL(input, parsedAcl, c.iam, acct.Role == auth.RoleAdmin)
+ if err != nil {
+ return &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: parsedAcl.Owner,
+ },
+ }, err
+ }
+
+ err = c.be.PutBucketAcl(ctx.Context(), bucket, updAcl)
+ return &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: parsedAcl.Owner,
+ },
+ }, err
+}
+
+func (c S3ApiController) CreateBucket(ctx *fiber.Ctx) (*Response, error) {
+ bucket := ctx.Params("bucket")
+ acl := ctx.Get("X-Amz-Acl")
+ grantFullControl := ctx.Get("X-Amz-Grant-Full-Control")
+ grantRead := ctx.Get("X-Amz-Grant-Read")
+ grantReadACP := ctx.Get("X-Amz-Grant-Read-Acp")
+ grantWrite := ctx.Get("X-Amz-Grant-Write")
+ grantWriteACP := ctx.Get("X-Amz-Grant-Write-Acp")
+ lockEnabled := strings.EqualFold(ctx.Get("X-Amz-Bucket-Object-Lock-Enabled"), "true")
+ acct := utils.ContextKeyAccount.Get(ctx).(auth.Account)
+ grants := grantFullControl + grantRead + grantReadACP + grantWrite + grantWriteACP
+ objectOwnership := types.ObjectOwnership(
+ ctx.Get("X-Amz-Object-Ownership", string(types.ObjectOwnershipBucketOwnerEnforced)),
+ )
+
+ if acct.Role != auth.RoleAdmin && acct.Role != auth.RoleUserPlus {
+ return &Response{
+ MetaOpts: &MetaOptions{},
+ }, s3err.GetAPIError(s3err.ErrAccessDenied)
+ }
+
+ // validate the bucket name
+ if ok := utils.IsValidBucketName(bucket); !ok {
+ return &Response{
+ MetaOpts: &MetaOptions{},
+ }, s3err.GetAPIError(s3err.ErrInvalidBucketName)
+ }
+
+ // validate the object ownership value
+ if ok := utils.IsValidOwnership(objectOwnership); !ok {
+ return &Response{
+ MetaOpts: &MetaOptions{},
+ }, s3err.APIError{
+ Code: "InvalidArgument",
+ Description: fmt.Sprintf("Invalid x-amz-object-ownership header: %v", objectOwnership),
+ HTTPStatusCode: http.StatusBadRequest,
+ }
+ }
+
+ if acl+grants != "" && objectOwnership == types.ObjectOwnershipBucketOwnerEnforced {
+ debuglogger.Logf("bucket acls are disabled for %v object ownership", objectOwnership)
+ return &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: acct.Access,
+ },
+ }, s3err.GetAPIError(s3err.ErrInvalidBucketAclWithObjectOwnership)
+ }
+
+ if acl != "" && grants != "" {
+ debuglogger.Logf("invalid request: %q (grants) %q (acl)", grants, acl)
+ return &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: acct.Access,
+ },
+ }, s3err.GetAPIError(s3err.ErrBothCannedAndHeaderGrants)
+ }
+
+ defACL := auth.ACL{
+ Owner: acct.Access,
+ }
+
+ updAcl, err := auth.UpdateACL(&auth.PutBucketAclInput{
+ GrantFullControl: &grantFullControl,
+ GrantRead: &grantRead,
+ GrantReadACP: &grantReadACP,
+ GrantWrite: &grantWrite,
+ GrantWriteACP: &grantWriteACP,
+ AccessControlPolicy: &auth.AccessControlPolicy{
+ Owner: &types.Owner{
+ ID: &acct.Access,
+ }},
+ ACL: types.BucketCannedACL(acl),
+ }, defACL, c.iam, acct.Role == auth.RoleAdmin)
+ if err != nil {
+ debuglogger.Logf("failed to update bucket acl: %v", err)
+ return &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: acct.Access,
+ },
+ }, err
+ }
+
+ err = c.be.CreateBucket(ctx.Context(), &s3.CreateBucketInput{
+ Bucket: &bucket,
+ ObjectOwnership: objectOwnership,
+ ObjectLockEnabledForBucket: &lockEnabled,
+ }, updAcl)
+ return &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: acct.Access,
+ },
+ }, err
+}
diff --git a/s3api/controllers/bucket-put_test.go b/s3api/controllers/bucket-put_test.go
new file mode 100644
index 00000000..239addc7
--- /dev/null
+++ b/s3api/controllers/bucket-put_test.go
@@ -0,0 +1,1101 @@
+// Copyright 2023 Versity Software
+// This file is licensed under the Apache License, Version 2.0
+// (the "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package controllers
+
+import (
+ "context"
+ "encoding/xml"
+ "errors"
+ "fmt"
+ "net/http"
+ "testing"
+
+ "github.com/aws/aws-sdk-go-v2/service/s3"
+ "github.com/aws/aws-sdk-go-v2/service/s3/types"
+ "github.com/stretchr/testify/assert"
+ "github.com/versity/versitygw/auth"
+ "github.com/versity/versitygw/s3api/utils"
+ "github.com/versity/versitygw/s3err"
+ "github.com/versity/versitygw/s3response"
+)
+
+func TestS3ApiController_PutBucketTagging(t *testing.T) {
+ validTaggingBody, err := xml.Marshal(s3response.TaggingInput{
+ TagSet: s3response.TagSet{
+ Tags: []s3response.Tag{
+ {
+ Key: "key",
+ Value: "val",
+ },
+ },
+ },
+ })
+ assert.NoError(t, err)
+
+ tests := []struct {
+ name string
+ input testInput
+ output testOutput
+ }{
+ {
+ name: "verify access fails",
+ input: testInput{
+ locals: accessDeniedLocals,
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ },
+ },
+ err: s3err.GetAPIError(s3err.ErrAccessDenied),
+ },
+ },
+ {
+ name: "invalid request body",
+ input: testInput{
+ locals: defaultLocals,
+ body: []byte("invalid_body"),
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ },
+ },
+ err: s3err.GetAPIError(s3err.ErrMalformedXML),
+ },
+ },
+ {
+ name: "backend returns error",
+ input: testInput{
+ locals: defaultLocals,
+ beErr: s3err.GetAPIError(s3err.ErrNoSuchBucket),
+ body: validTaggingBody,
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ Status: http.StatusNoContent,
+ },
+ },
+ err: s3err.GetAPIError(s3err.ErrNoSuchBucket),
+ },
+ },
+ {
+ name: "successful response",
+ input: testInput{
+ locals: defaultLocals,
+ body: validTaggingBody,
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ Status: http.StatusNoContent,
+ },
+ },
+ },
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ be := &BackendMock{
+ PutBucketTaggingFunc: func(contextMoqParam context.Context, bucket string, tags map[string]string) error {
+ return tt.input.beErr
+ },
+ GetBucketPolicyFunc: func(contextMoqParam context.Context, bucket string) ([]byte, error) {
+ return nil, s3err.GetAPIError(s3err.ErrAccessDenied)
+ },
+ }
+
+ ctrl := S3ApiController{
+ be: be,
+ }
+
+ testController(
+ t,
+ ctrl.PutBucketTagging,
+ tt.output.response,
+ tt.output.err,
+ ctxInputs{
+ locals: tt.input.locals,
+ body: tt.input.body,
+ })
+ })
+ }
+}
+
+func TestS3ApiController_PutBucketOwnershipControls(t *testing.T) {
+ validOwnershipBody, err := xml.Marshal(
+ s3response.OwnershipControls{
+ Rules: []types.OwnershipControlsRule{
+ {ObjectOwnership: types.ObjectOwnershipBucketOwnerEnforced},
+ },
+ })
+ assert.NoError(t, err)
+
+ invalidRuleCountBody, err := xml.Marshal(
+ s3response.OwnershipControls{
+ Rules: []types.OwnershipControlsRule{
+ {ObjectOwnership: types.ObjectOwnershipBucketOwnerEnforced},
+ {ObjectOwnership: types.ObjectOwnershipBucketOwnerPreferred},
+ },
+ },
+ )
+ assert.NoError(t, err)
+
+ tests := []struct {
+ name string
+ input testInput
+ output testOutput
+ }{
+ {
+ name: "verify access fails",
+ input: testInput{
+ locals: accessDeniedLocals,
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ },
+ },
+ err: s3err.GetAPIError(s3err.ErrAccessDenied),
+ },
+ },
+ {
+ name: "invalid request body",
+ input: testInput{
+ locals: defaultLocals,
+ body: []byte("invalid_body"),
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{BucketOwner: "root"},
+ },
+ err: s3err.GetAPIError(s3err.ErrMalformedXML),
+ },
+ },
+ {
+ name: "invalid rules count",
+ input: testInput{
+ locals: defaultLocals,
+ body: invalidRuleCountBody,
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{BucketOwner: "root"},
+ },
+ err: s3err.GetAPIError(s3err.ErrMalformedXML),
+ },
+ },
+ {
+ name: "backend error",
+ input: testInput{
+ locals: defaultLocals,
+ body: validOwnershipBody,
+ beErr: s3err.GetAPIError(s3err.ErrNoSuchBucket),
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{BucketOwner: "root"},
+ },
+ err: s3err.GetAPIError(s3err.ErrNoSuchBucket),
+ },
+ },
+ {
+ name: "success",
+ input: testInput{
+ locals: defaultLocals,
+ body: validOwnershipBody,
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ },
+ },
+ },
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ be := &BackendMock{
+ PutBucketOwnershipControlsFunc: func(contextMoqParam context.Context, bucket string, ownership types.ObjectOwnership) error {
+ return tt.input.beErr
+ },
+ GetBucketPolicyFunc: func(contextMoqParam context.Context, bucket string) ([]byte, error) {
+ return nil, s3err.GetAPIError(s3err.ErrAccessDenied)
+ },
+ }
+
+ ctrl := S3ApiController{
+ be: be,
+ }
+
+ testController(t, ctrl.PutBucketOwnershipControls, tt.output.response, tt.output.err, ctxInputs{
+ locals: tt.input.locals,
+ body: tt.input.body,
+ })
+ })
+ }
+}
+
+func TestS3ApiController_PutBucketVersioning(t *testing.T) {
+ validVersioningBody, err := xml.Marshal(
+ types.VersioningConfiguration{
+ Status: types.BucketVersioningStatusEnabled,
+ },
+ )
+ assert.NoError(t, err)
+
+ invalidVersioningStatusBody, err := xml.Marshal(
+ types.VersioningConfiguration{
+ Status: types.BucketVersioningStatus("invalid_status"),
+ },
+ )
+ assert.NoError(t, err)
+
+ tests := []struct {
+ name string
+ input testInput
+ output testOutput
+ }{
+ {
+ name: "verify access fails",
+ input: testInput{
+ locals: accessDeniedLocals,
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ },
+ },
+ err: s3err.GetAPIError(s3err.ErrAccessDenied),
+ },
+ },
+ {
+ name: "invalid request body",
+ input: testInput{
+ locals: defaultLocals,
+ body: []byte("invalid_body"),
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{BucketOwner: "root"},
+ },
+ err: s3err.GetAPIError(s3err.ErrInvalidRequest),
+ },
+ },
+ {
+ name: "invalid rules count",
+ input: testInput{
+ locals: defaultLocals,
+ body: invalidVersioningStatusBody,
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{BucketOwner: "root"},
+ },
+ err: s3err.GetAPIError(s3err.ErrMalformedXML),
+ },
+ },
+ {
+ name: "backend error",
+ input: testInput{
+ locals: defaultLocals,
+ body: validVersioningBody,
+ beErr: s3err.GetAPIError(s3err.ErrNoSuchBucket),
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{BucketOwner: "root"},
+ },
+ err: s3err.GetAPIError(s3err.ErrNoSuchBucket),
+ },
+ },
+ {
+ name: "success",
+ input: testInput{
+ locals: defaultLocals,
+ body: validVersioningBody,
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ },
+ },
+ },
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ be := &BackendMock{
+ PutBucketVersioningFunc: func(contextMoqParam context.Context, bucket string, status types.BucketVersioningStatus) error {
+ return tt.input.beErr
+ },
+ GetBucketPolicyFunc: func(contextMoqParam context.Context, bucket string) ([]byte, error) {
+ return nil, s3err.GetAPIError(s3err.ErrAccessDenied)
+ },
+ }
+
+ ctrl := S3ApiController{
+ be: be,
+ }
+
+ testController(t, ctrl.PutBucketVersioning, tt.output.response, tt.output.err, ctxInputs{
+ locals: tt.input.locals,
+ body: tt.input.body,
+ })
+ })
+ }
+}
+
+func TestS3ApiController_PutObjectLockConfiguration(t *testing.T) {
+ validLockBody, err := xml.Marshal(
+ types.ObjectLockConfiguration{
+ ObjectLockEnabled: types.ObjectLockEnabledEnabled,
+ },
+ )
+ assert.NoError(t, err)
+
+ tests := []struct {
+ name string
+ input testInput
+ output testOutput
+ }{
+ {
+ name: "verify access fails",
+ input: testInput{
+ locals: accessDeniedLocals,
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ },
+ },
+ err: s3err.GetAPIError(s3err.ErrAccessDenied),
+ },
+ },
+ {
+ name: "invalid request body",
+ input: testInput{
+ locals: defaultLocals,
+ body: []byte("invalid_body"),
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{BucketOwner: "root"},
+ },
+ err: s3err.GetAPIError(s3err.ErrMalformedXML),
+ },
+ },
+ {
+ name: "backend error",
+ input: testInput{
+ locals: defaultLocals,
+ body: validLockBody,
+ beErr: s3err.GetAPIError(s3err.ErrNoSuchBucket),
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{BucketOwner: "root"},
+ },
+ err: s3err.GetAPIError(s3err.ErrNoSuchBucket),
+ },
+ },
+ {
+ name: "success",
+ input: testInput{
+ locals: defaultLocals,
+ body: validLockBody,
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ },
+ },
+ },
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ be := &BackendMock{
+ PutObjectLockConfigurationFunc: func(contextMoqParam context.Context, bucket string, config []byte) error {
+ return tt.input.beErr
+ },
+ GetBucketPolicyFunc: func(contextMoqParam context.Context, bucket string) ([]byte, error) {
+ return nil, s3err.GetAPIError(s3err.ErrAccessDenied)
+ },
+ }
+
+ ctrl := S3ApiController{
+ be: be,
+ }
+
+ testController(t, ctrl.PutObjectLockConfiguration, tt.output.response, tt.output.err, ctxInputs{
+ locals: tt.input.locals,
+ body: tt.input.body,
+ })
+ })
+ }
+}
+
+func TestS3ApiController_PutBucketCors(t *testing.T) {
+ tests := []struct {
+ name string
+ input testInput
+ output testOutput
+ }{
+ {
+ name: "verify access fails",
+ input: testInput{
+ locals: accessDeniedLocals,
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ },
+ },
+ err: s3err.GetAPIError(s3err.ErrAccessDenied),
+ },
+ },
+ {
+ name: "backend error",
+ input: testInput{
+ locals: defaultLocals,
+ beErr: s3err.GetAPIError(s3err.ErrNotImplemented),
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{BucketOwner: "root"},
+ },
+ err: s3err.GetAPIError(s3err.ErrNotImplemented),
+ },
+ },
+ {
+ name: "success",
+ input: testInput{
+ locals: defaultLocals,
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ },
+ },
+ },
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ be := &BackendMock{
+ PutBucketCorsFunc: func(contextMoqParam context.Context, bytes []byte) error {
+ return tt.input.beErr
+ },
+ GetBucketPolicyFunc: func(contextMoqParam context.Context, bucket string) ([]byte, error) {
+ return nil, s3err.GetAPIError(s3err.ErrAccessDenied)
+ },
+ }
+
+ ctrl := S3ApiController{
+ be: be,
+ }
+
+ testController(t, ctrl.PutBucketCors, tt.output.response, tt.output.err, ctxInputs{
+ locals: tt.input.locals,
+ })
+ })
+ }
+}
+
+func TestS3ApiController_PutBucketPolicy(t *testing.T) {
+ validPolicyDocument :=
+ `{
+ "Version": "2012-10-17",
+ "Statement": [
+ {
+ "Sid": "PublicReadGetObject",
+ "Effect": "Allow",
+ "Principal": "*",
+ "Action": "s3:GetObject",
+ "Resource": "arn:aws:s3:::bucket/*"
+ }
+ ]
+ }`
+ tests := []struct {
+ name string
+ input testInput
+ output testOutput
+ }{
+ {
+ name: "verify access fails",
+ input: testInput{
+ locals: accessDeniedLocals,
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ },
+ },
+ err: s3err.GetAPIError(s3err.ErrAccessDenied),
+ },
+ },
+ {
+ name: "invalid policy document",
+ input: testInput{
+ locals: defaultLocals,
+ body: []byte("invalid_body"),
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{BucketOwner: "root"},
+ },
+ err: s3err.APIError{
+ Code: "MalformedPolicy",
+ Description: "Policies must be valid JSON and the first byte must be '{'",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ },
+ },
+ {
+ name: "backend error",
+ input: testInput{
+ locals: defaultLocals,
+ body: []byte(validPolicyDocument),
+ beErr: s3err.GetAPIError(s3err.ErrNoSuchBucket),
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{BucketOwner: "root"},
+ },
+ err: s3err.GetAPIError(s3err.ErrNoSuchBucket),
+ },
+ },
+ {
+ name: "success",
+ input: testInput{
+ locals: defaultLocals,
+ body: []byte(validPolicyDocument),
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ },
+ },
+ },
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ be := &BackendMock{
+ PutBucketPolicyFunc: func(contextMoqParam context.Context, bucket string, policy []byte) error {
+ return tt.input.beErr
+ },
+ GetBucketPolicyFunc: func(contextMoqParam context.Context, bucket string) ([]byte, error) {
+ return nil, s3err.GetAPIError(s3err.ErrAccessDenied)
+ },
+ }
+
+ ctrl := S3ApiController{
+ be: be,
+ }
+
+ testController(t, ctrl.PutBucketPolicy, tt.output.response, tt.output.err, ctxInputs{
+ locals: tt.input.locals,
+ body: tt.input.body,
+ })
+ })
+ }
+}
+
+func TestS3ApiController_CreateBucket(t *testing.T) {
+ adminAcc := auth.Account{
+ Access: "root",
+ Role: auth.RoleAdmin,
+ }
+ userAcc := auth.Account{
+ Access: "user",
+ Role: auth.RoleUser,
+ }
+
+ tests := []struct {
+ name string
+ input testInput
+ output testOutput
+ }{
+ {
+ name: "access denied",
+ input: testInput{
+ locals: map[utils.ContextKey]any{
+ utils.ContextKeyAccount: userAcc,
+ },
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{},
+ },
+ err: s3err.GetAPIError(s3err.ErrAccessDenied),
+ },
+ },
+ {
+ name: "invalid bucket name",
+ input: testInput{
+ locals: map[utils.ContextKey]any{
+ utils.ContextKeyAccount: adminAcc,
+ },
+ bucket: "invalid_bucket_name",
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{},
+ },
+ err: s3err.GetAPIError(s3err.ErrInvalidBucketName),
+ },
+ },
+ {
+ name: "invalid ownership",
+ input: testInput{
+ locals: map[utils.ContextKey]any{
+ utils.ContextKeyAccount: adminAcc,
+ },
+ headers: map[string]string{
+ "X-Amz-Object-Ownership": "invalid_ownership",
+ },
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{},
+ },
+ err: s3err.APIError{
+ Code: "InvalidArgument",
+ Description: "Invalid x-amz-object-ownership header: invalid_ownership",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ },
+ },
+ {
+ name: "invalid ownership + acl",
+ input: testInput{
+ locals: map[utils.ContextKey]any{
+ utils.ContextKeyAccount: adminAcc,
+ },
+ headers: map[string]string{
+ "X-Amz-Object-Ownership": string(types.ObjectOwnershipBucketOwnerEnforced),
+ "X-Amz-Acl": string(types.BucketCannedACLPublicRead),
+ },
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{BucketOwner: adminAcc.Access},
+ },
+ err: s3err.GetAPIError(s3err.ErrInvalidBucketAclWithObjectOwnership),
+ },
+ },
+ {
+ name: "both grants and canned acl",
+ input: testInput{
+ locals: map[utils.ContextKey]any{
+ utils.ContextKeyAccount: adminAcc,
+ },
+ headers: map[string]string{
+ "X-Amz-Acl": string(types.BucketCannedACLPublicRead),
+ "X-Amz-Grant-Read": userAcc.Access,
+ "X-Amz-Object-Ownership": string(types.ObjectOwnershipBucketOwnerPreferred),
+ },
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{BucketOwner: adminAcc.Access},
+ },
+ err: s3err.GetAPIError(s3err.ErrBothCannedAndHeaderGrants),
+ },
+ },
+ {
+ name: "fail to update the acl",
+ input: testInput{
+ locals: map[utils.ContextKey]any{
+ utils.ContextKeyAccount: adminAcc,
+ },
+ headers: map[string]string{
+ "X-Amz-Grant-Read": userAcc.Access,
+ "X-Amz-Object-Ownership": string(types.ObjectOwnershipBucketOwnerPreferred),
+ },
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{BucketOwner: adminAcc.Access},
+ },
+ err: fmt.Errorf("accounts does not exist: %s", userAcc.Access),
+ },
+ },
+ {
+ name: "backend error",
+ input: testInput{
+ locals: map[utils.ContextKey]any{
+ utils.ContextKeyAccount: adminAcc,
+ },
+ beErr: s3err.GetAPIError(s3err.ErrNoSuchBucket),
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{BucketOwner: adminAcc.Access},
+ },
+ err: s3err.GetAPIError(s3err.ErrNoSuchBucket),
+ },
+ },
+ {
+ name: "success",
+ input: testInput{
+ locals: map[utils.ContextKey]any{
+ utils.ContextKeyAccount: adminAcc,
+ },
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: adminAcc.Access,
+ },
+ },
+ },
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ be := &BackendMock{
+ CreateBucketFunc: func(contextMoqParam context.Context, createBucketInput *s3.CreateBucketInput, defaultACL []byte) error {
+ return tt.input.beErr
+ },
+ }
+
+ ctrl := S3ApiController{
+ be: be,
+ iam: auth.NewIAMServiceSingle(adminAcc),
+ }
+
+ testController(t, ctrl.CreateBucket, tt.output.response, tt.output.err,
+ ctxInputs{
+ locals: tt.input.locals,
+ body: tt.input.body,
+ bucket: tt.input.bucket,
+ headers: tt.input.headers,
+ })
+ })
+ }
+}
+
+func TestS3ApiController_PutBucketAcl(t *testing.T) {
+ invalidBody, err := xml.Marshal(auth.AccessControlPolicy{
+ Owner: &types.Owner{
+ ID: utils.GetStringPtr("root"),
+ },
+ AccessControlList: auth.AccessControlList{
+ Grants: []auth.Grant{
+ {
+ Permission: auth.Permission("invalid_permission"),
+ },
+ },
+ },
+ })
+ assert.NoError(t, err)
+
+ incorrectOwnerBody, err := xml.Marshal(auth.AccessControlPolicy{
+ Owner: &types.Owner{
+ ID: utils.GetStringPtr("user"),
+ },
+ AccessControlList: auth.AccessControlList{},
+ })
+ assert.NoError(t, err)
+
+ validAccessControlPolicy, err := xml.Marshal(auth.AccessControlPolicy{
+ Owner: &types.Owner{
+ ID: utils.GetStringPtr("root"),
+ },
+ AccessControlList: auth.AccessControlList{},
+ })
+ assert.NoError(t, err)
+
+ tests := []struct {
+ name string
+ input testInput
+ output testOutput
+ }{
+ {
+ name: "access denied",
+ input: testInput{
+ locals: accessDeniedLocals,
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ },
+ },
+ err: s3err.GetAPIError(s3err.ErrAccessDenied),
+ },
+ },
+ {
+ name: "fails to get bucket ownership",
+ input: testInput{
+ locals: defaultLocals,
+ extraMockErr: s3err.GetAPIError(s3err.ErrInternalError),
+ extraMockResp: types.ObjectOwnership(""),
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ },
+ },
+ err: s3err.GetAPIError(s3err.ErrInternalError),
+ },
+ },
+ {
+ name: "acl not supported",
+ input: testInput{
+ locals: defaultLocals,
+ extraMockResp: types.ObjectOwnershipBucketOwnerEnforced,
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ },
+ },
+ err: s3err.GetAPIError(s3err.ErrAclNotSupported),
+ },
+ },
+ {
+ name: "invalid request body",
+ input: testInput{
+ locals: defaultLocals,
+ extraMockResp: types.ObjectOwnershipBucketOwnerPreferred,
+ body: []byte("invalid_body"),
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ },
+ },
+ err: s3err.GetAPIError(s3err.ErrMalformedACL),
+ },
+ },
+ {
+ name: "invalid access control policy",
+ input: testInput{
+ locals: defaultLocals,
+ extraMockResp: types.ObjectOwnershipBucketOwnerPreferred,
+ body: invalidBody,
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ },
+ },
+ err: s3err.GetAPIError(s3err.ErrMalformedACL),
+ },
+ },
+ {
+ name: "incorrect owner id",
+ input: testInput{
+ locals: defaultLocals,
+ extraMockResp: types.ObjectOwnershipBucketOwnerPreferred,
+ body: incorrectOwnerBody,
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ },
+ },
+ err: s3err.APIError{
+ Code: "InvalidArgument",
+ Description: "Invalid id",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
+ },
+ },
+ {
+ name: "both access control policy and grants",
+ input: testInput{
+ body: validAccessControlPolicy,
+ extraMockResp: types.ObjectOwnershipBucketOwnerPreferred,
+ headers: map[string]string{
+ "X-Amz-Acl": "public-read",
+ },
+ locals: defaultLocals,
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ },
+ },
+ err: s3err.GetAPIError(s3err.ErrUnexpectedContent),
+ },
+ },
+ {
+ name: "access control policy success",
+ input: testInput{
+ body: validAccessControlPolicy,
+ extraMockResp: types.ObjectOwnershipBucketOwnerPreferred,
+ locals: defaultLocals,
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ },
+ },
+ },
+ },
+ {
+ name: "invalid canned acl",
+ input: testInput{
+ extraMockResp: types.ObjectOwnershipBucketOwnerPreferred,
+ headers: map[string]string{
+ "X-Amz-Acl": "invalid_acl",
+ },
+ locals: defaultLocals,
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ },
+ },
+ err: s3err.GetAPIError(s3err.ErrInvalidRequest),
+ },
+ },
+ {
+ name: "both canned acl and grants",
+ input: testInput{
+ extraMockResp: types.ObjectOwnershipBucketOwnerPreferred,
+ headers: map[string]string{
+ "X-Amz-Acl": "public-read",
+ "X-Amz-Grant-Read": "grt1",
+ },
+ locals: defaultLocals,
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ },
+ },
+ err: s3err.GetAPIError(s3err.ErrBothCannedAndHeaderGrants),
+ },
+ },
+ {
+ name: "canned acl success",
+ input: testInput{
+ extraMockResp: types.ObjectOwnershipBucketOwnerPreferred,
+ headers: map[string]string{
+ "X-Amz-Acl": "public-read",
+ },
+ locals: defaultLocals,
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ },
+ },
+ },
+ },
+ {
+ name: "grants update acl fails",
+ input: testInput{
+ extraMockResp: types.ObjectOwnershipBucketOwnerPreferred,
+ headers: map[string]string{
+ "X-Amz-Grant-Read": "grt1",
+ },
+ locals: defaultLocals,
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ },
+ },
+ err: errors.New("accounts does not exist: grt1"),
+ },
+ },
+ {
+ name: "no option provided",
+ input: testInput{
+ extraMockResp: types.ObjectOwnershipBucketOwnerPreferred,
+ locals: defaultLocals,
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ },
+ },
+ err: s3err.GetAPIError(s3err.ErrMissingSecurityHeader),
+ },
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ be := &BackendMock{
+ PutBucketAclFunc: func(contextMoqParam context.Context, bucket string, data []byte) error {
+ return tt.input.beErr
+ },
+ GetBucketOwnershipControlsFunc: func(contextMoqParam context.Context, bucket string) (types.ObjectOwnership, error) {
+ return tt.input.extraMockResp.(types.ObjectOwnership), tt.input.extraMockErr
+ },
+ GetBucketPolicyFunc: func(contextMoqParam context.Context, bucket string) ([]byte, error) {
+ return nil, s3err.GetAPIError(s3err.ErrAccessDenied)
+ },
+ }
+
+ ctrl := S3ApiController{
+ be: be,
+ iam: auth.NewIAMServiceSingle(
+ auth.Account{
+ Access: "root",
+ }),
+ }
+
+ testController(t, ctrl.PutBucketAcl, tt.output.response, tt.output.err,
+ ctxInputs{
+ locals: tt.input.locals,
+ body: tt.input.body,
+ bucket: tt.input.bucket,
+ headers: tt.input.headers,
+ })
+ })
+ }
+}
diff --git a/s3api/controllers/object-delete.go b/s3api/controllers/object-delete.go
new file mode 100644
index 00000000..76dfb167
--- /dev/null
+++ b/s3api/controllers/object-delete.go
@@ -0,0 +1,198 @@
+// Copyright 2023 Versity Software
+// This file is licensed under the Apache License, Version 2.0
+// (the "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package controllers
+
+import (
+ "fmt"
+ "net/http"
+ "strings"
+
+ "github.com/aws/aws-sdk-go-v2/service/s3"
+ "github.com/aws/aws-sdk-go-v2/service/s3/types"
+ "github.com/gofiber/fiber/v2"
+ "github.com/versity/versitygw/auth"
+ "github.com/versity/versitygw/s3api/utils"
+ "github.com/versity/versitygw/s3event"
+)
+
+func (c S3ApiController) DeleteObjectTagging(ctx *fiber.Ctx) (*Response, error) {
+ bucket := ctx.Params("bucket")
+ key := strings.TrimPrefix(ctx.Path(), fmt.Sprintf("/%s/", bucket))
+ acct := utils.ContextKeyAccount.Get(ctx).(auth.Account)
+ isRoot := utils.ContextKeyIsRoot.Get(ctx).(bool)
+ isBucketPublic := utils.ContextKeyPublicBucket.IsSet(ctx)
+ 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.DeleteObjectTaggingAction,
+ IsBucketPublic: isBucketPublic,
+ })
+ if err != nil {
+ return &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: parsedAcl.Owner,
+ },
+ }, err
+ }
+
+ err = c.be.DeleteObjectTagging(ctx.Context(), bucket, key)
+ return &Response{
+ MetaOpts: &MetaOptions{
+ Status: http.StatusNoContent,
+ BucketOwner: parsedAcl.Owner,
+ EventName: s3event.EventObjectTaggingDelete,
+ },
+ }, err
+}
+
+func (c S3ApiController) AbortMultipartUpload(ctx *fiber.Ctx) (*Response, error) {
+ bucket := ctx.Params("bucket")
+ key := strings.TrimPrefix(ctx.Path(), fmt.Sprintf("/%s/", bucket))
+ uploadId := ctx.Query("uploadId")
+ acct := utils.ContextKeyAccount.Get(ctx).(auth.Account)
+ isRoot := utils.ContextKeyIsRoot.Get(ctx).(bool)
+ isBucketPublic := utils.ContextKeyPublicBucket.IsSet(ctx)
+ 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.AbortMultipartUploadAction,
+ IsBucketPublic: isBucketPublic,
+ })
+ if err != nil {
+ return &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: parsedAcl.Owner,
+ },
+ }, err
+ }
+
+ err = c.be.AbortMultipartUpload(ctx.Context(),
+ &s3.AbortMultipartUploadInput{
+ UploadId: &uploadId,
+ Bucket: &bucket,
+ Key: &key,
+ })
+ return &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: parsedAcl.Owner,
+ Status: http.StatusNoContent,
+ },
+ }, err
+}
+
+func (c S3ApiController) DeleteObject(ctx *fiber.Ctx) (*Response, error) {
+ bucket := ctx.Params("bucket")
+ key := strings.TrimPrefix(ctx.Path(), fmt.Sprintf("/%s/", bucket))
+ versionId := ctx.Query("versionId")
+ bypass := strings.EqualFold(ctx.Get("X-Amz-Bypass-Governance-Retention"), "true")
+ // context locals
+ acct := utils.ContextKeyAccount.Get(ctx).(auth.Account)
+ isRoot := utils.ContextKeyIsRoot.Get(ctx).(bool)
+ isBucketPublic := utils.ContextKeyPublicBucket.IsSet(ctx)
+ parsedAcl := utils.ContextKeyParsedAcl.Get(ctx).(auth.ACL)
+
+ //TODO: check s3:DeleteObjectVersion policy in case a use tries to delete a version of an object
+
+ 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,
+ })
+ if err != nil {
+ return &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: parsedAcl.Owner,
+ },
+ }, err
+ }
+
+ err = auth.CheckObjectAccess(
+ ctx.Context(),
+ bucket,
+ acct.Access,
+ []types.ObjectIdentifier{
+ {
+ Key: &key,
+ VersionId: &versionId,
+ },
+ },
+ bypass,
+ isBucketPublic,
+ c.be,
+ )
+ if err != nil {
+ return &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: parsedAcl.Owner,
+ },
+ }, err
+ }
+
+ res, err := c.be.DeleteObject(ctx.Context(),
+ &s3.DeleteObjectInput{
+ Bucket: &bucket,
+ Key: &key,
+ VersionId: &versionId,
+ })
+ if err != nil {
+ return &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: parsedAcl.Owner,
+ EventName: s3event.EventObjectRemovedDelete,
+ Status: http.StatusNoContent,
+ },
+ }, err
+ }
+
+ headers := map[string]*string{
+ "x-amz-version-id": res.VersionId,
+ }
+
+ if res.DeleteMarker != nil && *res.DeleteMarker {
+ headers["x-amz-delete-marker"] = utils.GetStringPtr("true")
+ }
+
+ return &Response{
+ Headers: headers,
+ MetaOpts: &MetaOptions{
+ BucketOwner: parsedAcl.Owner,
+ EventName: s3event.EventObjectRemovedDelete,
+ Status: http.StatusNoContent,
+ },
+ }, nil
+}
diff --git a/s3api/controllers/object-delete_test.go b/s3api/controllers/object-delete_test.go
new file mode 100644
index 00000000..d9054351
--- /dev/null
+++ b/s3api/controllers/object-delete_test.go
@@ -0,0 +1,296 @@
+// Copyright 2023 Versity Software
+// This file is licensed under the Apache License, Version 2.0
+// (the "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package controllers
+
+import (
+ "context"
+ "net/http"
+ "testing"
+
+ "github.com/aws/aws-sdk-go-v2/service/s3"
+ "github.com/versity/versitygw/s3api/utils"
+ "github.com/versity/versitygw/s3err"
+ "github.com/versity/versitygw/s3event"
+)
+
+func TestS3ApiController_DeleteObjectTagging(t *testing.T) {
+ tests := []struct {
+ name string
+ input testInput
+ output testOutput
+ }{
+ {
+ name: "verify access fails",
+ input: testInput{
+ locals: accessDeniedLocals,
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ },
+ },
+ err: s3err.GetAPIError(s3err.ErrAccessDenied),
+ },
+ },
+ {
+ name: "backend returns error",
+ input: testInput{
+ locals: defaultLocals,
+ beErr: s3err.GetAPIError(s3err.ErrInvalidRequest),
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ Status: http.StatusNoContent,
+ EventName: s3event.EventObjectTaggingDelete,
+ },
+ },
+ err: s3err.GetAPIError(s3err.ErrInvalidRequest),
+ },
+ },
+ {
+ name: "successful response",
+ input: testInput{
+ locals: defaultLocals,
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ Status: http.StatusNoContent,
+ EventName: s3event.EventObjectTaggingDelete,
+ },
+ },
+ },
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ be := &BackendMock{
+ DeleteObjectTaggingFunc: func(contextMoqParam context.Context, bucket, object string) error {
+ return tt.input.beErr
+ },
+ GetBucketPolicyFunc: func(contextMoqParam context.Context, bucket string) ([]byte, error) {
+ return nil, s3err.GetAPIError(s3err.ErrAccessDenied)
+ },
+ }
+
+ ctrl := S3ApiController{
+ be: be,
+ }
+
+ testController(
+ t,
+ ctrl.DeleteObjectTagging,
+ tt.output.response,
+ tt.output.err,
+ ctxInputs{
+ locals: tt.input.locals,
+ })
+ })
+ }
+}
+
+func TestS3ApiController_AbortMultipartUpload(t *testing.T) {
+ tests := []struct {
+ name string
+ input testInput
+ output testOutput
+ }{
+ {
+ name: "verify access fails",
+ input: testInput{
+ locals: accessDeniedLocals,
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ },
+ },
+ err: s3err.GetAPIError(s3err.ErrAccessDenied),
+ },
+ },
+ {
+ name: "backend returns error",
+ input: testInput{
+ locals: defaultLocals,
+ beErr: s3err.GetAPIError(s3err.ErrInvalidRequest),
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ Status: http.StatusNoContent,
+ },
+ },
+ err: s3err.GetAPIError(s3err.ErrInvalidRequest),
+ },
+ },
+ {
+ name: "successful response",
+ input: testInput{
+ locals: defaultLocals,
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ Status: http.StatusNoContent,
+ },
+ },
+ },
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ be := &BackendMock{
+ AbortMultipartUploadFunc: func(contextMoqParam context.Context, abortMultipartUploadInput *s3.AbortMultipartUploadInput) error {
+ return tt.input.beErr
+ },
+ GetBucketPolicyFunc: func(contextMoqParam context.Context, bucket string) ([]byte, error) {
+ return nil, s3err.GetAPIError(s3err.ErrAccessDenied)
+ },
+ }
+
+ ctrl := S3ApiController{
+ be: be,
+ }
+
+ testController(
+ t,
+ ctrl.AbortMultipartUpload,
+ tt.output.response,
+ tt.output.err,
+ ctxInputs{
+ locals: tt.input.locals,
+ })
+ })
+ }
+}
+
+func TestS3ApiController_DeleteObject(t *testing.T) {
+ delMarker, versionId := true, "versionId"
+ var emptyRes *s3.DeleteObjectOutput
+
+ tests := []struct {
+ name string
+ input testInput
+ output testOutput
+ }{
+ {
+ name: "verify access fails",
+ input: testInput{
+ locals: accessDeniedLocals,
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ },
+ },
+ err: s3err.GetAPIError(s3err.ErrAccessDenied),
+ },
+ },
+ {
+ name: "object locked",
+ input: testInput{
+ locals: defaultLocals,
+ extraMockErr: s3err.GetAPIError(s3err.ErrObjectLocked),
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ },
+ },
+ err: s3err.GetAPIError(s3err.ErrObjectLocked),
+ },
+ },
+ {
+ name: "backend returns error",
+ input: testInput{
+ locals: defaultLocals,
+ beErr: s3err.GetAPIError(s3err.ErrInvalidRequest),
+ extraMockErr: s3err.GetAPIError(s3err.ErrObjectLockConfigurationNotFound),
+ beRes: emptyRes,
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ Status: http.StatusNoContent,
+ EventName: s3event.EventObjectRemovedDelete,
+ },
+ },
+ err: s3err.GetAPIError(s3err.ErrInvalidRequest),
+ },
+ },
+ {
+ name: "successful response",
+ input: testInput{
+ locals: defaultLocals,
+ extraMockErr: s3err.GetAPIError(s3err.ErrObjectLockConfigurationNotFound),
+ beRes: &s3.DeleteObjectOutput{
+ DeleteMarker: &delMarker,
+ VersionId: &versionId,
+ },
+ },
+ output: testOutput{
+ response: &Response{
+ Headers: map[string]*string{
+ "x-amz-delete-marker": utils.GetStringPtr("true"),
+ "x-amz-version-id": &versionId,
+ },
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ Status: http.StatusNoContent,
+ EventName: s3event.EventObjectRemovedDelete,
+ },
+ },
+ },
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ be := &BackendMock{
+ DeleteObjectFunc: func(contextMoqParam context.Context, deleteObjectInput *s3.DeleteObjectInput) (*s3.DeleteObjectOutput, error) {
+ return tt.input.beRes.(*s3.DeleteObjectOutput), tt.input.beErr
+ },
+ GetBucketPolicyFunc: func(contextMoqParam context.Context, bucket string) ([]byte, error) {
+ return nil, s3err.GetAPIError(s3err.ErrAccessDenied)
+ },
+ GetObjectLockConfigurationFunc: func(contextMoqParam context.Context, bucket string) ([]byte, error) {
+ return nil, tt.input.extraMockErr
+ },
+ }
+
+ ctrl := S3ApiController{
+ be: be,
+ }
+
+ testController(
+ t,
+ ctrl.DeleteObject,
+ tt.output.response,
+ tt.output.err,
+ ctxInputs{
+ locals: tt.input.locals,
+ })
+ })
+ }
+}
diff --git a/s3api/controllers/object-get.go b/s3api/controllers/object-get.go
new file mode 100644
index 00000000..9fd7104c
--- /dev/null
+++ b/s3api/controllers/object-get.go
@@ -0,0 +1,509 @@
+// Copyright 2023 Versity Software
+// This file is licensed under the Apache License, Version 2.0
+// (the "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package controllers
+
+import (
+ "fmt"
+ "math"
+ "net/http"
+ "strconv"
+ "strings"
+ "time"
+
+ "github.com/aws/aws-sdk-go-v2/service/s3"
+ "github.com/aws/aws-sdk-go-v2/service/s3/types"
+ "github.com/gofiber/fiber/v2"
+ "github.com/versity/versitygw/auth"
+ "github.com/versity/versitygw/s3api/debuglogger"
+ "github.com/versity/versitygw/s3api/utils"
+ "github.com/versity/versitygw/s3err"
+ "github.com/versity/versitygw/s3response"
+)
+
+func (c S3ApiController) GetObjectTagging(ctx *fiber.Ctx) (*Response, error) {
+ bucket := ctx.Params("bucket")
+ key := strings.TrimPrefix(ctx.Path(), fmt.Sprintf("/%s/", bucket))
+ 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)
+
+ 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,
+ })
+ if err != nil {
+ return &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: parsedAcl.Owner,
+ },
+ }, err
+ }
+
+ data, err := c.be.GetObjectTagging(ctx.Context(), bucket, key)
+ if err != nil {
+ return &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: parsedAcl.Owner,
+ },
+ }, err
+ }
+ tags := s3response.Tagging{
+ TagSet: s3response.TagSet{Tags: []s3response.Tag{}},
+ }
+
+ for key, val := range data {
+ tags.TagSet.Tags = append(tags.TagSet.Tags,
+ s3response.Tag{Key: key, Value: val})
+ }
+
+ return &Response{
+ Data: tags,
+ MetaOpts: &MetaOptions{
+ BucketOwner: parsedAcl.Owner,
+ },
+ }, err
+}
+
+func (c S3ApiController) GetObjectRetention(ctx *fiber.Ctx) (*Response, error) {
+ bucket := ctx.Params("bucket")
+ key := strings.TrimPrefix(ctx.Path(), fmt.Sprintf("/%s/", bucket))
+ versionId := ctx.Query("versionId")
+ // 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)
+
+ 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,
+ })
+ if err != nil {
+ return &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: parsedAcl.Owner,
+ },
+ }, err
+ }
+
+ data, err := c.be.GetObjectRetention(ctx.Context(), bucket, key, versionId)
+ if err != nil {
+ return &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: parsedAcl.Owner,
+ },
+ }, err
+ }
+
+ retention, err := auth.ParseObjectLockRetentionOutput(data)
+ return &Response{
+ Data: retention,
+ MetaOpts: &MetaOptions{
+ BucketOwner: parsedAcl.Owner,
+ },
+ }, err
+}
+
+func (c S3ApiController) GetObjectLegalHold(ctx *fiber.Ctx) (*Response, error) {
+ bucket := ctx.Params("bucket")
+ key := strings.TrimPrefix(ctx.Path(), fmt.Sprintf("/%s/", bucket))
+ versionId := ctx.Query("versionId")
+ // 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)
+
+ 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,
+ })
+ if err != nil {
+ return &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: parsedAcl.Owner,
+ },
+ }, err
+ }
+
+ data, err := c.be.GetObjectLegalHold(ctx.Context(), bucket, key, versionId)
+ return &Response{
+ Data: auth.ParseObjectLegalHoldOutput(data),
+ MetaOpts: &MetaOptions{
+ BucketOwner: parsedAcl.Owner,
+ },
+ }, err
+}
+
+func (c S3ApiController) GetObjectAcl(ctx *fiber.Ctx) (*Response, error) {
+ bucket := ctx.Params("bucket")
+ key := strings.TrimPrefix(ctx.Path(), fmt.Sprintf("/%s/", bucket))
+ // 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)
+
+ 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,
+ })
+ if err != nil {
+ return &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: parsedAcl.Owner,
+ },
+ }, err
+ }
+ res, err := c.be.GetObjectAcl(ctx.Context(), &s3.GetObjectAclInput{
+ Bucket: &bucket,
+ Key: &key,
+ })
+ return &Response{
+ Data: res,
+ MetaOpts: &MetaOptions{
+ BucketOwner: parsedAcl.Owner,
+ },
+ }, err
+}
+
+func (c S3ApiController) ListParts(ctx *fiber.Ctx) (*Response, error) {
+ bucket := ctx.Params("bucket")
+ key := strings.TrimPrefix(ctx.Path(), fmt.Sprintf("/%s/", bucket))
+ uploadId := ctx.Query("uploadId")
+ partNumberMarker := ctx.Query("part-number-marker")
+ maxPartsStr := ctx.Query("max-parts")
+ // 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)
+
+ 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,
+ })
+ if err != nil {
+ return &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: parsedAcl.Owner,
+ },
+ }, err
+ }
+
+ // parse the part number marker
+ if partNumberMarker != "" {
+ n, err := strconv.Atoi(partNumberMarker)
+ if err != nil || n < 0 {
+ debuglogger.Logf("invalid part number marker %q: %v",
+ partNumberMarker, err)
+
+ return &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: parsedAcl.Owner,
+ },
+ }, s3err.GetAPIError(s3err.ErrInvalidPartNumberMarker)
+ }
+ }
+
+ // parse the max parts
+ maxParts, err := utils.ParseUint(maxPartsStr)
+ if err != nil {
+ debuglogger.Logf("error parsing max parts %q: %v",
+ maxPartsStr, err)
+ return &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: parsedAcl.Owner,
+ },
+ }, s3err.GetAPIError(s3err.ErrInvalidMaxParts)
+ }
+
+ res, err := c.be.ListParts(ctx.Context(), &s3.ListPartsInput{
+ Bucket: &bucket,
+ Key: &key,
+ UploadId: &uploadId,
+ PartNumberMarker: &partNumberMarker,
+ MaxParts: &maxParts,
+ })
+ return &Response{
+ Data: res,
+ MetaOpts: &MetaOptions{
+ BucketOwner: parsedAcl.Owner,
+ },
+ }, err
+}
+
+func (c S3ApiController) GetObjectAttributes(ctx *fiber.Ctx) (*Response, error) {
+ bucket := ctx.Params("bucket")
+ key := strings.TrimPrefix(ctx.Path(), fmt.Sprintf("/%s/", bucket))
+ versionId := ctx.Query("versionId")
+ maxPartsStr := ctx.Get("X-Amz-Max-Parts")
+ partNumberMarker := ctx.Get("X-Amz-Part-Number-Marker")
+ // 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)
+
+ 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,
+ })
+ if err != nil {
+ return &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: parsedAcl.Owner,
+ },
+ }, err
+ }
+
+ // parse max parts
+ maxParts, err := utils.ParseUint(maxPartsStr)
+ if err != nil {
+ debuglogger.Logf("error parsing max parts %q: %v",
+ maxPartsStr, err)
+ return &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: parsedAcl.Owner,
+ },
+ }, s3err.GetAPIError(s3err.ErrInvalidMaxParts)
+ }
+
+ // parse the object attributes
+ attrs, err := utils.ParseObjectAttributes(ctx)
+ if err != nil {
+ return &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: parsedAcl.Owner,
+ },
+ }, err
+ }
+
+ res, err := c.be.GetObjectAttributes(ctx.Context(),
+ &s3.GetObjectAttributesInput{
+ Bucket: &bucket,
+ Key: &key,
+ PartNumberMarker: &partNumberMarker,
+ MaxParts: &maxParts,
+ VersionId: &versionId,
+ })
+ if err != nil {
+ headers := map[string]*string{
+ "x-amz-version-id": res.VersionId,
+ }
+ if res.DeleteMarker != nil && *res.DeleteMarker {
+ headers["x-amz-delete-marker"] = utils.GetStringPtr("true")
+ }
+ return &Response{
+ Headers: headers,
+ MetaOpts: &MetaOptions{
+ BucketOwner: parsedAcl.Owner,
+ },
+ }, err
+ }
+
+ headers := map[string]*string{
+ "x-amz-version-id": res.VersionId,
+ "Last-Modified": utils.FormatDatePtrToString(res.LastModified, iso8601TimeFormatExtended),
+ }
+ if res.DeleteMarker != nil && *res.DeleteMarker {
+ headers["x-amz-delete-marker"] = utils.GetStringPtr("true")
+ }
+
+ return &Response{
+ Headers: headers,
+ Data: utils.FilterObjectAttributes(attrs, res),
+ MetaOpts: &MetaOptions{
+ BucketOwner: parsedAcl.Owner,
+ },
+ }, err
+}
+
+func (c S3ApiController) GetObject(ctx *fiber.Ctx) (*Response, error) {
+ bucket := ctx.Params("bucket")
+ key := strings.TrimPrefix(ctx.Path(), fmt.Sprintf("/%s/", bucket))
+ versionId := ctx.Query("versionId")
+ acceptRange := ctx.Get("Range")
+ checksumMode := types.ChecksumMode(ctx.Get("x-amz-checksum-mode"))
+ // 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)
+ utils.ContextKeySkipResBodyLog.Set(ctx, true)
+
+ action := auth.GetObjectAction
+ if 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,
+ })
+ if err != nil {
+ return &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: parsedAcl.Owner,
+ },
+ }, err
+ }
+
+ // validate the checksum mode
+ if checksumMode != "" && checksumMode != types.ChecksumModeEnabled {
+ debuglogger.Logf("invalid x-amz-checksum-mode header value: %v", checksumMode)
+ return &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: parsedAcl.Owner,
+ },
+ }, s3err.GetInvalidChecksumHeaderErr("x-amz-checksum-mode")
+ }
+
+ res, err := c.be.GetObject(ctx.Context(), &s3.GetObjectInput{
+ Bucket: &bucket,
+ Key: &key,
+ Range: &acceptRange,
+ VersionId: &versionId,
+ ChecksumMode: checksumMode,
+ })
+ if err != nil {
+ var headers map[string]*string
+ if res != nil {
+ headers = map[string]*string{
+ "x-amz-delete-marker": utils.GetStringPtr("true"),
+ "Last-Modified": utils.FormatDatePtrToString(res.LastModified, timefmt),
+ }
+ }
+ return &Response{
+ Headers: headers,
+ MetaOpts: &MetaOptions{
+ BucketOwner: parsedAcl.Owner,
+ },
+ }, err
+ }
+
+ // Set x-amz-meta-... headers
+ utils.SetMetaHeaders(ctx, res.Metadata)
+
+ status := http.StatusOK
+ if acceptRange != "" {
+ status = http.StatusPartialContent
+ }
+
+ if res.Body != nil {
+ // -1 will stream response body until EOF if content length not set
+ contentLen := -1
+ if res.ContentLength != nil {
+ if *res.ContentLength > int64(math.MaxInt) {
+ debuglogger.Logf("content length %v int overflow",
+ *res.ContentLength)
+ return &Response{
+ MetaOpts: &MetaOptions{
+ ContentLength: utils.GetInt64(res.ContentLength),
+ BucketOwner: parsedAcl.Owner,
+ Status: status,
+ },
+ }, s3err.GetAPIError(s3err.ErrInvalidRange)
+ }
+ contentLen = int(*res.ContentLength)
+ }
+ utils.StreamResponseBody(ctx, res.Body, contentLen)
+ }
+
+ return &Response{
+ Headers: map[string]*string{
+ "ETag": res.ETag,
+ "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,
+ "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,
+ "x-amz-version-id": res.VersionId,
+ "Content-Length": utils.ConvertPtrToStringPtr(res.ContentLength),
+ "x-amz-mp-parts-count": utils.ConvertPtrToStringPtr(res.PartsCount),
+ "x-amz-tagging-count": utils.ConvertPtrToStringPtr(res.TagCount),
+ "x-amz-object-lock-mode": utils.ConvertToStringPtr(res.ObjectLockMode),
+ "x-amz-object-lock-legal-hold": utils.ConvertToStringPtr(res.ObjectLockLegalHoldStatus),
+ "x-amz-storage-class": utils.ConvertToStringPtr(res.StorageClass),
+ "x-amz-checksum-type": utils.ConvertToStringPtr(res.ChecksumType),
+ "x-amz-object-lock-retain-until-date": utils.FormatDatePtrToString(res.ObjectLockRetainUntilDate, time.RFC3339),
+ "Last-Modified": utils.FormatDatePtrToString(res.LastModified, timefmt),
+ },
+ MetaOpts: &MetaOptions{
+ ContentLength: utils.GetInt64(res.ContentLength),
+ BucketOwner: parsedAcl.Owner,
+ Status: status,
+ },
+ }, nil
+}
diff --git a/s3api/controllers/object-get_test.go b/s3api/controllers/object-get_test.go
new file mode 100644
index 00000000..5f3f0a0f
--- /dev/null
+++ b/s3api/controllers/object-get_test.go
@@ -0,0 +1,820 @@
+// Copyright 2023 Versity Software
+// This file is licensed under the Apache License, Version 2.0
+// (the "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package controllers
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "io"
+ "net/http"
+ "strings"
+ "testing"
+ "time"
+
+ "github.com/aws/aws-sdk-go-v2/service/s3"
+ "github.com/aws/aws-sdk-go-v2/service/s3/types"
+ "github.com/stretchr/testify/assert"
+ "github.com/versity/versitygw/s3api/utils"
+ "github.com/versity/versitygw/s3err"
+ "github.com/versity/versitygw/s3response"
+)
+
+func TestS3ApiController_GetObjectTagging(t *testing.T) {
+ tests := []struct {
+ name string
+ input testInput
+ output testOutput
+ }{
+ {
+ name: "verify access fails",
+ input: testInput{
+ locals: accessDeniedLocals,
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ },
+ },
+ err: s3err.GetAPIError(s3err.ErrAccessDenied),
+ },
+ },
+ {
+ name: "backend returns error",
+ input: testInput{
+ locals: defaultLocals,
+ beRes: map[string]string{},
+ beErr: s3err.GetAPIError(s3err.ErrNoSuchBucket),
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ },
+ },
+ err: s3err.GetAPIError(s3err.ErrNoSuchBucket),
+ },
+ },
+ {
+ name: "successful response",
+ input: testInput{
+ locals: defaultLocals,
+ beRes: map[string]string{
+ "key": "val",
+ },
+ },
+ output: testOutput{
+ response: &Response{
+ Data: s3response.Tagging{
+ TagSet: s3response.TagSet{
+ Tags: []s3response.Tag{
+ {Key: "key", Value: "val"},
+ },
+ },
+ },
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ },
+ },
+ },
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ be := &BackendMock{
+ GetObjectTaggingFunc: func(contextMoqParam context.Context, bucket, object string) (map[string]string, error) {
+ return tt.input.beRes.(map[string]string), tt.input.beErr
+ },
+ GetBucketPolicyFunc: func(contextMoqParam context.Context, bucket string) ([]byte, error) {
+ return nil, s3err.GetAPIError(s3err.ErrAccessDenied)
+ },
+ }
+
+ ctrl := S3ApiController{
+ be: be,
+ }
+
+ testController(
+ t,
+ ctrl.GetObjectTagging,
+ tt.output.response,
+ tt.output.err,
+ ctxInputs{
+ locals: tt.input.locals,
+ body: tt.input.body,
+ })
+ })
+ }
+}
+
+func TestS3ApiController_GetObjectRetention(t *testing.T) {
+ retBytes, err := json.Marshal(types.ObjectLockRetention{
+ Mode: types.ObjectLockRetentionModeCompliance,
+ })
+ assert.NoError(t, err)
+
+ var retention *types.ObjectLockRetention
+
+ tests := []struct {
+ name string
+ input testInput
+ output testOutput
+ }{
+ {
+ name: "verify access fails",
+ input: testInput{
+ locals: accessDeniedLocals,
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ },
+ },
+ err: s3err.GetAPIError(s3err.ErrAccessDenied),
+ },
+ },
+ {
+ name: "backend returns error",
+ input: testInput{
+ locals: defaultLocals,
+ beRes: []byte{},
+ beErr: s3err.GetAPIError(s3err.ErrNoSuchBucket),
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ },
+ },
+ err: s3err.GetAPIError(s3err.ErrNoSuchBucket),
+ },
+ },
+ {
+ name: "invalid data from backend",
+ input: testInput{
+ locals: defaultLocals,
+ beRes: []byte{},
+ },
+ output: testOutput{
+ response: &Response{
+ Data: retention,
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ },
+ },
+ err: fmt.Errorf("parse object lock retention: "),
+ },
+ },
+ {
+ name: "successful response",
+ input: testInput{
+ locals: defaultLocals,
+ beRes: retBytes,
+ },
+ output: testOutput{
+ response: &Response{
+ Data: &types.ObjectLockRetention{
+ Mode: types.ObjectLockRetentionModeCompliance,
+ },
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ },
+ },
+ },
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ be := &BackendMock{
+ GetObjectRetentionFunc: func(contextMoqParam context.Context, bucket, object, versionId string) ([]byte, error) {
+ return tt.input.beRes.([]byte), tt.input.beErr
+ },
+ GetBucketPolicyFunc: func(contextMoqParam context.Context, bucket string) ([]byte, error) {
+ return nil, s3err.GetAPIError(s3err.ErrAccessDenied)
+ },
+ }
+
+ ctrl := S3ApiController{
+ be: be,
+ }
+
+ testController(
+ t,
+ ctrl.GetObjectRetention,
+ tt.output.response,
+ tt.output.err,
+ ctxInputs{
+ locals: tt.input.locals,
+ body: tt.input.body,
+ })
+ })
+ }
+}
+
+func TestS3ApiController_GetObjectLegalHold(t *testing.T) {
+ var legalHold *bool
+ var emptyLegalHold *s3response.GetObjectLegalHoldResult
+ status := true
+
+ tests := []struct {
+ name string
+ input testInput
+ output testOutput
+ }{
+ {
+ name: "verify access fails",
+ input: testInput{
+ locals: accessDeniedLocals,
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ },
+ },
+ err: s3err.GetAPIError(s3err.ErrAccessDenied),
+ },
+ },
+ {
+ name: "backend returns error",
+ input: testInput{
+ locals: defaultLocals,
+ beRes: legalHold,
+ beErr: s3err.GetAPIError(s3err.ErrNoSuchBucket),
+ },
+ output: testOutput{
+ response: &Response{
+ Data: emptyLegalHold,
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ },
+ },
+ err: s3err.GetAPIError(s3err.ErrNoSuchBucket),
+ },
+ },
+ {
+ name: "successful response",
+ input: testInput{
+ locals: defaultLocals,
+ beRes: &status,
+ },
+ output: testOutput{
+ response: &Response{
+ Data: &s3response.GetObjectLegalHoldResult{
+ Status: types.ObjectLockLegalHoldStatusOn,
+ },
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ },
+ },
+ },
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ be := &BackendMock{
+ GetObjectLegalHoldFunc: func(contextMoqParam context.Context, bucket, object, versionId string) (*bool, error) {
+ return tt.input.beRes.(*bool), tt.input.beErr
+ },
+ GetBucketPolicyFunc: func(contextMoqParam context.Context, bucket string) ([]byte, error) {
+ return nil, s3err.GetAPIError(s3err.ErrAccessDenied)
+ },
+ }
+
+ ctrl := S3ApiController{
+ be: be,
+ }
+
+ testController(
+ t,
+ ctrl.GetObjectLegalHold,
+ tt.output.response,
+ tt.output.err,
+ ctxInputs{
+ locals: tt.input.locals,
+ body: tt.input.body,
+ })
+ })
+ }
+}
+
+func TestS3ApiController_GetObjectAcl(t *testing.T) {
+ var emptyRes *s3.GetObjectAclOutput
+ tests := []struct {
+ name string
+ input testInput
+ output testOutput
+ }{
+ {
+ name: "verify access fails",
+ input: testInput{
+ locals: accessDeniedLocals,
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ },
+ },
+ err: s3err.GetAPIError(s3err.ErrAccessDenied),
+ },
+ },
+ {
+ name: "backend returns error",
+ input: testInput{
+ locals: defaultLocals,
+ beRes: emptyRes,
+ beErr: s3err.GetAPIError(s3err.ErrNotImplemented),
+ },
+ output: testOutput{
+ response: &Response{
+ Data: emptyRes,
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ },
+ },
+ err: s3err.GetAPIError(s3err.ErrNotImplemented),
+ },
+ },
+ {
+ name: "successful response",
+ input: testInput{
+ locals: defaultLocals,
+ beRes: &s3.GetObjectAclOutput{
+ Owner: &types.Owner{
+ ID: utils.GetStringPtr("something"),
+ },
+ },
+ },
+ output: testOutput{
+ response: &Response{
+ Data: &s3.GetObjectAclOutput{
+ Owner: &types.Owner{
+ ID: utils.GetStringPtr("something"),
+ },
+ },
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ },
+ },
+ },
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ be := &BackendMock{
+ GetObjectAclFunc: func(contextMoqParam context.Context, getObjectAclInput *s3.GetObjectAclInput) (*s3.GetObjectAclOutput, error) {
+ return tt.input.beRes.(*s3.GetObjectAclOutput), tt.input.beErr
+ },
+ GetBucketPolicyFunc: func(contextMoqParam context.Context, bucket string) ([]byte, error) {
+ return nil, s3err.GetAPIError(s3err.ErrAccessDenied)
+ },
+ }
+
+ ctrl := S3ApiController{
+ be: be,
+ }
+
+ testController(
+ t,
+ ctrl.GetObjectAcl,
+ tt.output.response,
+ tt.output.err,
+ ctxInputs{
+ locals: tt.input.locals,
+ body: tt.input.body,
+ })
+ })
+ }
+}
+
+func TestS3ApiController_ListParts(t *testing.T) {
+ listPartsResult := s3response.ListPartsResult{
+ Bucket: "my-bucket",
+ Key: "obj",
+ IsTruncated: false,
+ Parts: []s3response.Part{
+ {ETag: "ETag"},
+ },
+ }
+ tests := []struct {
+ name string
+ input testInput
+ output testOutput
+ }{
+ {
+ name: "verify access fails",
+ input: testInput{
+ locals: accessDeniedLocals,
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ },
+ },
+ err: s3err.GetAPIError(s3err.ErrAccessDenied),
+ },
+ },
+ {
+ name: "invalid part number marker",
+ input: testInput{
+ locals: defaultLocals,
+ queries: map[string]string{
+ "part-number-marker": "-1",
+ },
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ },
+ },
+ err: s3err.GetAPIError(s3err.ErrInvalidPartNumberMarker),
+ },
+ },
+ {
+ name: "invalid max parts",
+ input: testInput{
+ locals: defaultLocals,
+ queries: map[string]string{
+ "max-parts": "-1",
+ },
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ },
+ },
+ err: s3err.GetAPIError(s3err.ErrInvalidMaxParts),
+ },
+ },
+ {
+ name: "backend returns error",
+ input: testInput{
+ locals: defaultLocals,
+ beRes: s3response.ListPartsResult{},
+ beErr: s3err.GetAPIError(s3err.ErrNoSuchBucket),
+ },
+ output: testOutput{
+ response: &Response{
+ Data: s3response.ListPartsResult{},
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ },
+ },
+ err: s3err.GetAPIError(s3err.ErrNoSuchBucket),
+ },
+ },
+ {
+ name: "successful response",
+ input: testInput{
+ locals: defaultLocals,
+ beRes: listPartsResult,
+ },
+ output: testOutput{
+ response: &Response{
+ Data: listPartsResult,
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ },
+ },
+ },
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ be := &BackendMock{
+ ListPartsFunc: func(contextMoqParam context.Context, listPartsInput *s3.ListPartsInput) (s3response.ListPartsResult, error) {
+ return tt.input.beRes.(s3response.ListPartsResult), tt.input.beErr
+ },
+ GetBucketPolicyFunc: func(contextMoqParam context.Context, bucket string) ([]byte, error) {
+ return nil, s3err.GetAPIError(s3err.ErrAccessDenied)
+ },
+ }
+
+ ctrl := S3ApiController{
+ be: be,
+ }
+
+ testController(
+ t,
+ ctrl.ListParts,
+ tt.output.response,
+ tt.output.err,
+ ctxInputs{
+ locals: tt.input.locals,
+ body: tt.input.body,
+ queries: tt.input.queries,
+ })
+ })
+ }
+}
+
+func TestS3ApiController_GetObjectAttributes(t *testing.T) {
+ delMarker, lastModTime, etag := true, time.Now(), "ETag"
+ timeFormatted := lastModTime.UTC().Format(iso8601TimeFormatExtended)
+
+ validRes := s3response.GetObjectAttributesResponse{
+ DeleteMarker: &delMarker,
+ LastModified: &lastModTime,
+ VersionId: utils.GetStringPtr("versionId"),
+ ETag: &etag,
+ }
+ tests := []struct {
+ name string
+ input testInput
+ output testOutput
+ }{
+ {
+ name: "verify access fails",
+ input: testInput{
+ locals: accessDeniedLocals,
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ },
+ },
+ err: s3err.GetAPIError(s3err.ErrAccessDenied),
+ },
+ },
+ {
+ name: "invalid max parts",
+ input: testInput{
+ locals: defaultLocals,
+ headers: map[string]string{
+ "X-Amz-Max-Parts": "-1",
+ },
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ },
+ },
+ err: s3err.GetAPIError(s3err.ErrInvalidMaxParts),
+ },
+ },
+ {
+ name: "invalid object attributes",
+ input: testInput{
+ locals: defaultLocals,
+ headers: map[string]string{
+ "X-Amz-Object-Attributes": "invalid_attribute",
+ },
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ },
+ },
+ err: s3err.GetAPIError(s3err.ErrInvalidObjectAttributes),
+ },
+ },
+ {
+ name: "backend returns error",
+ input: testInput{
+ locals: defaultLocals,
+ beRes: validRes,
+ beErr: s3err.GetAPIError(s3err.ErrNoSuchBucket),
+ headers: map[string]string{
+ "X-Amz-Object-Attributes": "ETag",
+ },
+ },
+ output: testOutput{
+ response: &Response{
+ Headers: map[string]*string{
+ "x-amz-version-id": utils.GetStringPtr("versionId"),
+ "x-amz-delete-marker": utils.GetStringPtr("true"),
+ },
+ Data: nil,
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ },
+ },
+ err: s3err.GetAPIError(s3err.ErrNoSuchBucket),
+ },
+ },
+ {
+ name: "successful response",
+ input: testInput{
+ locals: defaultLocals,
+ beRes: validRes,
+ headers: map[string]string{
+ "X-Amz-Object-Attributes": "ETag",
+ },
+ },
+ output: testOutput{
+ response: &Response{
+ Headers: map[string]*string{
+ "x-amz-version-id": utils.GetStringPtr("versionId"),
+ "x-amz-delete-marker": utils.GetStringPtr("true"),
+ "Last-Modified": &timeFormatted,
+ },
+ Data: s3response.GetObjectAttributesResponse{
+ ETag: &etag,
+ },
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ },
+ },
+ },
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ be := &BackendMock{
+ GetObjectAttributesFunc: func(contextMoqParam context.Context, getObjectAttributesInput *s3.GetObjectAttributesInput) (s3response.GetObjectAttributesResponse, error) {
+ return tt.input.beRes.(s3response.GetObjectAttributesResponse), tt.input.beErr
+ },
+ GetBucketPolicyFunc: func(contextMoqParam context.Context, bucket string) ([]byte, error) {
+ return nil, s3err.GetAPIError(s3err.ErrAccessDenied)
+ },
+ }
+
+ ctrl := S3ApiController{
+ be: be,
+ }
+
+ testController(
+ t,
+ ctrl.GetObjectAttributes,
+ tt.output.response,
+ tt.output.err,
+ ctxInputs{
+ locals: tt.input.locals,
+ body: tt.input.body,
+ headers: tt.input.headers,
+ })
+ })
+ }
+}
+
+func TestS3ApiController_GetObject(t *testing.T) {
+ tm := time.Now()
+ cLength := int64(11)
+ rdr := io.NopCloser(strings.NewReader("hello world"))
+ delMarker := true
+
+ tests := []struct {
+ name string
+ input testInput
+ output testOutput
+ }{
+ {
+ name: "verify access fails",
+ input: testInput{
+ locals: accessDeniedLocals,
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ },
+ },
+ err: s3err.GetAPIError(s3err.ErrAccessDenied),
+ },
+ },
+ {
+ name: "invalid checksum mode",
+ input: testInput{
+ locals: defaultLocals,
+ headers: map[string]string{
+ "x-amz-checksum-mode": "invalid_checksum_mode",
+ },
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ },
+ },
+ err: s3err.GetInvalidChecksumHeaderErr("x-amz-checksum-mode"),
+ },
+ },
+ {
+ name: "backend returns error",
+ input: testInput{
+ locals: defaultLocals,
+ beErr: s3err.GetAPIError(s3err.ErrInvalidAccessKeyID),
+ beRes: &s3.GetObjectOutput{
+ DeleteMarker: &delMarker,
+ LastModified: &tm,
+ },
+ },
+ output: testOutput{
+ response: &Response{
+ Headers: map[string]*string{
+ "x-amz-delete-marker": utils.GetStringPtr("true"),
+ "Last-Modified": utils.GetStringPtr(tm.UTC().Format(timefmt)),
+ },
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ },
+ },
+ err: s3err.GetAPIError(s3err.ErrInvalidAccessKeyID),
+ },
+ },
+ // TODO: add a test case for overflowing content-length
+ // simulate a 32 bit arch to test the case
+ {
+ name: "successful response",
+ input: testInput{
+ headers: map[string]string{
+ "Range": "100-200",
+ },
+ queries: map[string]string{
+ "versionId": "versionId",
+ },
+ locals: defaultLocals,
+ beRes: &s3.GetObjectOutput{
+ ETag: utils.GetStringPtr("ETag"),
+ ContentType: utils.GetStringPtr("application/xml"),
+ ContentLength: &cLength,
+ Body: rdr,
+ },
+ },
+ output: testOutput{
+ response: &Response{
+ Headers: map[string]*string{
+ "ETag": utils.GetStringPtr("ETag"),
+ "x-amz-restore": nil,
+ "accept-ranges": nil,
+ "Content-Range": nil,
+ "Content-Disposition": nil,
+ "Content-Encoding": nil,
+ "Content-Language": nil,
+ "Cache-Control": nil,
+ "Expires": nil,
+ "x-amz-checksum-crc32": nil,
+ "x-amz-checksum-crc64nvme": nil,
+ "x-amz-checksum-crc32c": nil,
+ "x-amz-checksum-sha1": nil,
+ "x-amz-checksum-sha256": nil,
+ "x-amz-version-id": nil,
+ "x-amz-mp-parts-count": nil,
+ "x-amz-object-lock-mode": nil,
+ "x-amz-object-lock-legal-hold": nil,
+ "x-amz-storage-class": nil,
+ "x-amz-checksum-type": nil,
+ "x-amz-object-lock-retain-until-date": nil,
+ "Last-Modified": nil,
+ "x-amz-tagging-count": nil,
+ "Content-Type": utils.GetStringPtr("application/xml"),
+ "Content-Length": utils.GetStringPtr("11"),
+ },
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ Status: http.StatusPartialContent,
+ ContentLength: cLength,
+ },
+ },
+ },
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ be := &BackendMock{
+ GetObjectFunc: func(contextMoqParam context.Context, getObjectInput *s3.GetObjectInput) (*s3.GetObjectOutput, error) {
+ return tt.input.beRes.(*s3.GetObjectOutput), tt.input.beErr
+ },
+ GetBucketPolicyFunc: func(contextMoqParam context.Context, bucket string) ([]byte, error) {
+ return nil, s3err.GetAPIError(s3err.ErrAccessDenied)
+ },
+ }
+
+ ctrl := S3ApiController{
+ be: be,
+ }
+
+ testController(
+ t,
+ ctrl.GetObject,
+ tt.output.response,
+ tt.output.err,
+ ctxInputs{
+ locals: tt.input.locals,
+ headers: tt.input.headers,
+ queries: tt.input.queries,
+ })
+ })
+ }
+}
diff --git a/s3api/controllers/object-head.go b/s3api/controllers/object-head.go
new file mode 100644
index 00000000..57c749b0
--- /dev/null
+++ b/s3api/controllers/object-head.go
@@ -0,0 +1,147 @@
+// Copyright 2023 Versity Software
+// This file is licensed under the Apache License, Version 2.0
+// (the "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package controllers
+
+import (
+ "fmt"
+ "strings"
+ "time"
+
+ "github.com/aws/aws-sdk-go-v2/service/s3"
+ "github.com/aws/aws-sdk-go-v2/service/s3/types"
+ "github.com/gofiber/fiber/v2"
+ "github.com/versity/versitygw/auth"
+ "github.com/versity/versitygw/s3api/debuglogger"
+ "github.com/versity/versitygw/s3api/utils"
+ "github.com/versity/versitygw/s3err"
+)
+
+func (c S3ApiController) HeadObject(ctx *fiber.Ctx) (*Response, error) {
+ // 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)
+ // url values
+ bucket := ctx.Params("bucket")
+ partNumberQuery := int32(ctx.QueryInt("partNumber", -1))
+ versionId := ctx.Query("versionId")
+ objRange := ctx.Get("Range")
+ key := strings.TrimPrefix(ctx.Path(), fmt.Sprintf("/%s/", bucket))
+
+ 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: isPublicBucket,
+ })
+ if err != nil {
+ return &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: parsedAcl.Owner,
+ },
+ }, err
+ }
+
+ var partNumber *int32
+ if ctx.Request().URI().QueryArgs().Has("partNumber") {
+ if partNumberQuery < minPartNumber || partNumberQuery > maxPartNumber {
+ debuglogger.Logf("invalid part number: %d", partNumberQuery)
+ return &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: parsedAcl.Owner,
+ },
+ }, s3err.GetAPIError(s3err.ErrInvalidPartNumber)
+ }
+
+ partNumber = &partNumberQuery
+ }
+
+ checksumMode := types.ChecksumMode(ctx.Get("x-amz-checksum-mode"))
+ if checksumMode != "" && checksumMode != types.ChecksumModeEnabled {
+ debuglogger.Logf("invalid x-amz-checksum-mode header value: %v", checksumMode)
+ return &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: parsedAcl.Owner,
+ },
+ }, s3err.GetInvalidChecksumHeaderErr("x-amz-checksum-mode")
+ }
+
+ res, err := c.be.HeadObject(ctx.Context(),
+ &s3.HeadObjectInput{
+ Bucket: &bucket,
+ Key: &key,
+ PartNumber: partNumber,
+ VersionId: &versionId,
+ ChecksumMode: checksumMode,
+ Range: &objRange,
+ })
+ if err != nil {
+ var headers map[string]*string
+ if res != nil {
+ headers = map[string]*string{
+ "x-amz-delete-marker": utils.GetStringPtr("true"),
+ "Last-Modified": utils.GetStringPtr(res.LastModified.UTC().Format(timefmt)),
+ }
+ }
+ return &Response{
+ Headers: headers,
+ MetaOpts: &MetaOptions{
+ BucketOwner: parsedAcl.Owner,
+ },
+ }, err
+ }
+
+ // Set the metadata headers
+ utils.SetMetaHeaders(ctx, res.Metadata)
+
+ return &Response{
+ Headers: map[string]*string{
+ "ETag": res.ETag,
+ "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,
+ "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,
+ "x-amz-version-id": res.VersionId,
+ "Content-Length": utils.ConvertPtrToStringPtr(res.ContentLength),
+ "x-amz-mp-parts-count": utils.ConvertPtrToStringPtr(res.PartsCount),
+ "x-amz-object-lock-mode": utils.ConvertToStringPtr(res.ObjectLockMode),
+ "x-amz-object-lock-legal-hold": utils.ConvertToStringPtr(res.ObjectLockLegalHoldStatus),
+ "x-amz-storage-class": utils.ConvertToStringPtr(res.StorageClass),
+ "x-amz-checksum-type": utils.ConvertToStringPtr(res.ChecksumType),
+ "x-amz-object-lock-retain-until-date": utils.FormatDatePtrToString(res.ObjectLockRetainUntilDate, time.RFC3339),
+ "Last-Modified": utils.FormatDatePtrToString(res.LastModified, timefmt),
+ },
+ MetaOpts: &MetaOptions{
+ BucketOwner: parsedAcl.Owner,
+ },
+ }, nil
+}
diff --git a/s3api/controllers/object-head_test.go b/s3api/controllers/object-head_test.go
new file mode 100644
index 00000000..9d243e33
--- /dev/null
+++ b/s3api/controllers/object-head_test.go
@@ -0,0 +1,183 @@
+// Copyright 2023 Versity Software
+// This file is licensed under the Apache License, Version 2.0
+// (the "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package controllers
+
+import (
+ "context"
+ "testing"
+ "time"
+
+ "github.com/aws/aws-sdk-go-v2/service/s3"
+ "github.com/versity/versitygw/s3api/utils"
+ "github.com/versity/versitygw/s3err"
+)
+
+func TestS3ApiController_HeadObject(t *testing.T) {
+ tm := time.Now()
+ cLength := int64(100)
+
+ failingBeRes := &s3.HeadObjectOutput{
+ LastModified: &tm,
+ }
+
+ tests := []struct {
+ name string
+ input testInput
+ output testOutput
+ }{
+ {
+ name: "verify access fails",
+ input: testInput{
+ locals: accessDeniedLocals,
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ },
+ },
+ err: s3err.GetAPIError(s3err.ErrAccessDenied),
+ },
+ },
+ {
+ name: "invalid part number",
+ input: testInput{
+ locals: defaultLocals,
+ queries: map[string]string{
+ "partNumber": "-4",
+ },
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ },
+ },
+ err: s3err.GetAPIError(s3err.ErrInvalidPartNumber),
+ },
+ },
+ {
+ name: "invalid checksum mode",
+ input: testInput{
+ locals: defaultLocals,
+ headers: map[string]string{
+ "x-amz-checksum-mode": "invalid_checksum_mode",
+ },
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ },
+ },
+ err: s3err.GetInvalidChecksumHeaderErr("x-amz-checksum-mode"),
+ },
+ },
+ {
+ name: "backend returns error",
+ input: testInput{
+ locals: defaultLocals,
+ beErr: s3err.GetAPIError(s3err.ErrInvalidAccessKeyID),
+ beRes: failingBeRes,
+ },
+ output: testOutput{
+ response: &Response{
+ Headers: map[string]*string{
+ "x-amz-delete-marker": utils.GetStringPtr("true"),
+ "Last-Modified": utils.GetStringPtr(tm.UTC().Format(timefmt)),
+ },
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ },
+ },
+ err: s3err.GetAPIError(s3err.ErrInvalidAccessKeyID),
+ },
+ },
+ {
+ name: "successful response",
+ input: testInput{
+ queries: map[string]string{
+ "partNumber": "4",
+ },
+ locals: defaultLocals,
+ beRes: &s3.HeadObjectOutput{
+ ETag: utils.GetStringPtr("ETag"),
+ ContentType: utils.GetStringPtr("application/xml"),
+ ContentLength: &cLength,
+ },
+ },
+ output: testOutput{
+ response: &Response{
+ Headers: map[string]*string{
+ "ETag": utils.GetStringPtr("ETag"),
+ "x-amz-restore": nil,
+ "accept-ranges": nil,
+ "Content-Range": nil,
+ "Content-Disposition": nil,
+ "Content-Encoding": nil,
+ "Content-Language": nil,
+ "Cache-Control": nil,
+ "Expires": nil,
+ "x-amz-checksum-crc32": nil,
+ "x-amz-checksum-crc64nvme": nil,
+ "x-amz-checksum-crc32c": nil,
+ "x-amz-checksum-sha1": nil,
+ "x-amz-checksum-sha256": nil,
+ "x-amz-version-id": nil,
+ "x-amz-mp-parts-count": nil,
+ "x-amz-object-lock-mode": nil,
+ "x-amz-object-lock-legal-hold": nil,
+ "x-amz-storage-class": nil,
+ "x-amz-checksum-type": nil,
+ "x-amz-object-lock-retain-until-date": nil,
+ "Last-Modified": nil,
+ "Content-Type": utils.GetStringPtr("application/xml"),
+ "Content-Length": utils.GetStringPtr("100"),
+ },
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ },
+ },
+ },
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ be := &BackendMock{
+ HeadObjectFunc: func(contextMoqParam context.Context, headObjectInput *s3.HeadObjectInput) (*s3.HeadObjectOutput, error) {
+ return tt.input.beRes.(*s3.HeadObjectOutput), tt.input.beErr
+ },
+ GetBucketPolicyFunc: func(contextMoqParam context.Context, bucket string) ([]byte, error) {
+ return nil, s3err.GetAPIError(s3err.ErrAccessDenied)
+ },
+ }
+
+ ctrl := S3ApiController{
+ be: be,
+ }
+
+ testController(
+ t,
+ ctrl.HeadObject,
+ tt.output.response,
+ tt.output.err,
+ ctxInputs{
+ locals: tt.input.locals,
+ queries: tt.input.queries,
+ headers: tt.input.headers,
+ })
+ })
+ }
+}
diff --git a/s3api/controllers/object-post.go b/s3api/controllers/object-post.go
new file mode 100644
index 00000000..35fece82
--- /dev/null
+++ b/s3api/controllers/object-post.go
@@ -0,0 +1,353 @@
+// Copyright 2023 Versity Software
+// This file is licensed under the Apache License, Version 2.0
+// (the "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package controllers
+
+import (
+ "encoding/xml"
+ "fmt"
+ "strconv"
+ "strings"
+
+ "github.com/aws/aws-sdk-go-v2/service/s3"
+ "github.com/aws/aws-sdk-go-v2/service/s3/types"
+ "github.com/gofiber/fiber/v2"
+ "github.com/versity/versitygw/auth"
+ "github.com/versity/versitygw/s3api/debuglogger"
+ "github.com/versity/versitygw/s3api/utils"
+ "github.com/versity/versitygw/s3err"
+ "github.com/versity/versitygw/s3event"
+ "github.com/versity/versitygw/s3response"
+)
+
+func (c S3ApiController) RestoreObject(ctx *fiber.Ctx) (*Response, error) {
+ bucket := ctx.Params("bucket")
+ key := strings.TrimPrefix(ctx.Path(), fmt.Sprintf("/%s/", bucket))
+ acct := utils.ContextKeyAccount.Get(ctx).(auth.Account)
+ isRoot := utils.ContextKeyIsRoot.Get(ctx).(bool)
+ isBucketPublic := utils.ContextKeyPublicBucket.IsSet(ctx)
+ 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.RestoreObjectAction,
+ IsBucketPublic: isBucketPublic,
+ })
+ if err != nil {
+ return &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: parsedAcl.Owner,
+ },
+ }, err
+ }
+
+ var restoreRequest types.RestoreRequest
+ if err := xml.Unmarshal(ctx.Body(), &restoreRequest); err != nil {
+ debuglogger.Logf("failed to parse the request body: %v", err)
+ return &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: parsedAcl.Owner,
+ },
+ }, s3err.GetAPIError(s3err.ErrMalformedXML)
+ }
+
+ err = c.be.RestoreObject(ctx.Context(), &s3.RestoreObjectInput{
+ Bucket: &bucket,
+ Key: &key,
+ RestoreRequest: &restoreRequest,
+ })
+ return &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: parsedAcl.Owner,
+ EventName: s3event.EventObjectRestoreCompleted,
+ },
+ }, err
+}
+
+func (c S3ApiController) SelectObjectContent(ctx *fiber.Ctx) (*Response, error) {
+ bucket := ctx.Params("bucket")
+ key := strings.TrimPrefix(ctx.Path(), fmt.Sprintf("/%s/", bucket))
+ acct := utils.ContextKeyAccount.Get(ctx).(auth.Account)
+ isRoot := utils.ContextKeyIsRoot.Get(ctx).(bool)
+ isBucketPublic := utils.ContextKeyPublicBucket.IsSet(ctx)
+ 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,
+ Object: key,
+ Action: auth.GetObjectAction,
+ IsBucketPublic: isBucketPublic,
+ })
+ if err != nil {
+ return &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: parsedAcl.Owner,
+ },
+ }, err
+ }
+
+ var payload s3response.SelectObjectContentPayload
+ err = xml.Unmarshal(ctx.Body(), &payload)
+ if err != nil {
+ debuglogger.Logf("error unmarshalling select object content: %v", err)
+ return &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: parsedAcl.Owner,
+ },
+ }, s3err.GetAPIError(s3err.ErrMalformedXML)
+ }
+
+ sw := c.be.SelectObjectContent(ctx.Context(),
+ &s3.SelectObjectContentInput{
+ Bucket: &bucket,
+ Key: &key,
+ Expression: payload.Expression,
+ ExpressionType: payload.ExpressionType,
+ InputSerialization: payload.InputSerialization,
+ OutputSerialization: payload.OutputSerialization,
+ RequestProgress: payload.RequestProgress,
+ ScanRange: payload.ScanRange,
+ })
+
+ ctx.Context().SetBodyStreamWriter(sw)
+
+ return &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: parsedAcl.Owner,
+ },
+ }, nil
+}
+
+func (c S3ApiController) CreateMultipartUpload(ctx *fiber.Ctx) (*Response, error) {
+ bucket := ctx.Params("bucket")
+ key := strings.TrimPrefix(ctx.Path(), fmt.Sprintf("/%s/", bucket))
+ contentType := ctx.Get("Content-Type")
+ contentDisposition := ctx.Get("Content-Disposition")
+ contentLanguage := ctx.Get("Content-Language")
+ cacheControl := ctx.Get("Cache-Control")
+ contentEncoding := ctx.Get("Content-Encoding")
+ tagging := ctx.Get("X-Amz-Tagging")
+ expires := ctx.Get("Expires")
+ metadata := utils.GetUserMetaData(&ctx.Request().Header)
+ // context locals
+ acct := utils.ContextKeyAccount.Get(ctx).(auth.Account)
+ isRoot := utils.ContextKeyIsRoot.Get(ctx).(bool)
+ 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.PutObjectAction,
+ })
+ if err != nil {
+ return &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: parsedAcl.Owner,
+ },
+ }, err
+ }
+
+ objLockState, err := utils.ParsObjectLockHdrs(ctx)
+ if err != nil {
+ return &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: parsedAcl.Owner,
+ },
+ }, err
+ }
+
+ checksumAlgorithm, checksumType, err := utils.ParseCreateMpChecksumHeaders(ctx)
+ if err != nil {
+ return &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: parsedAcl.Owner,
+ },
+ }, err
+ }
+
+ res, err := c.be.CreateMultipartUpload(ctx.Context(),
+ s3response.CreateMultipartUploadInput{
+ Bucket: &bucket,
+ Key: &key,
+ Tagging: &tagging,
+ ContentType: &contentType,
+ ContentEncoding: &contentEncoding,
+ ContentDisposition: &contentDisposition,
+ ContentLanguage: &contentLanguage,
+ CacheControl: &cacheControl,
+ Expires: &expires,
+ ObjectLockRetainUntilDate: &objLockState.RetainUntilDate,
+ ObjectLockMode: objLockState.ObjectLockMode,
+ ObjectLockLegalHoldStatus: objLockState.LegalHoldStatus,
+ Metadata: metadata,
+ ChecksumAlgorithm: checksumAlgorithm,
+ ChecksumType: checksumType,
+ })
+ var headers map[string]*string
+ if err == nil {
+ headers = map[string]*string{
+ "x-amz-checksum-algorithm": utils.ConvertToStringPtr(checksumAlgorithm),
+ }
+ }
+ return &Response{
+ Headers: headers,
+ Data: res,
+ MetaOpts: &MetaOptions{
+ BucketOwner: parsedAcl.Owner,
+ },
+ }, err
+}
+
+func (c S3ApiController) CompleteMultipartUpload(ctx *fiber.Ctx) (*Response, error) {
+ bucket := ctx.Params("bucket")
+ key := strings.TrimPrefix(ctx.Path(), fmt.Sprintf("/%s/", bucket))
+ uploadId := ctx.Query("uploadId")
+ mpuObjSizeHdr := ctx.Get("X-Amz-Mp-Object-Size")
+ checksumType := types.ChecksumType(ctx.Get("x-amz-checksum-type"))
+ // context locals
+ acct := utils.ContextKeyAccount.Get(ctx).(auth.Account)
+ isRoot := utils.ContextKeyIsRoot.Get(ctx).(bool)
+ isBucketPublic := utils.ContextKeyPublicBucket.IsSet(ctx)
+ 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.PutObjectAction,
+ IsBucketPublic: isBucketPublic,
+ })
+ if err != nil {
+ return &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: parsedAcl.Owner,
+ },
+ }, err
+ }
+
+ var body s3response.CompleteMultipartUploadRequestBody
+ err = xml.Unmarshal(ctx.Body(), &body)
+ if err != nil {
+ debuglogger.Logf("error unmarshalling complete multipart upload: %v", err)
+ return &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: parsedAcl.Owner,
+ },
+ }, s3err.GetAPIError(s3err.ErrMalformedXML)
+ }
+
+ if len(body.Parts) == 0 {
+ debuglogger.Logf("empty parts provided for complete multipart upload")
+ return &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: parsedAcl.Owner,
+ },
+ }, s3err.GetAPIError(s3err.ErrEmptyParts)
+ }
+
+ var mpuObjectSize *int64
+ if mpuObjSizeHdr != "" {
+ val, err := strconv.ParseInt(mpuObjSizeHdr, 10, 64)
+ if err != nil {
+ debuglogger.Logf("invalid value for 'x-amz-mp-object-size' header: %v", err)
+ return &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: parsedAcl.Owner,
+ },
+ }, s3err.GetInvalidMpObjectSizeErr(mpuObjSizeHdr)
+ }
+
+ if val < 0 {
+ debuglogger.Logf("value for 'x-amz-mp-object-size' header is less than 0: %v", val)
+ return &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: parsedAcl.Owner,
+ },
+ }, s3err.GetNegatvieMpObjectSizeErr(val)
+ }
+
+ mpuObjectSize = &val
+ }
+
+ _, checksums, err := utils.ParseChecksumHeaders(ctx)
+ if err != nil {
+ return &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: parsedAcl.Owner,
+ },
+ }, err
+ }
+
+ err = utils.IsChecksumTypeValid(checksumType)
+ if err != nil {
+ return &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: parsedAcl.Owner,
+ },
+ }, err
+ }
+
+ res, versid, err := c.be.CompleteMultipartUpload(ctx.Context(),
+ &s3.CompleteMultipartUploadInput{
+ Bucket: &bucket,
+ Key: &key,
+ UploadId: &uploadId,
+ MultipartUpload: &types.CompletedMultipartUpload{
+ Parts: body.Parts,
+ },
+ MpuObjectSize: mpuObjectSize,
+ ChecksumCRC32: utils.GetStringPtr(checksums[types.ChecksumAlgorithmCrc32]),
+ ChecksumCRC32C: utils.GetStringPtr(checksums[types.ChecksumAlgorithmCrc32c]),
+ ChecksumSHA1: utils.GetStringPtr(checksums[types.ChecksumAlgorithmSha1]),
+ ChecksumSHA256: utils.GetStringPtr(checksums[types.ChecksumAlgorithmSha256]),
+ ChecksumCRC64NVME: utils.GetStringPtr(checksums[types.ChecksumAlgorithmCrc64nvme]),
+ ChecksumType: checksumType,
+ })
+ return &Response{
+ Data: res,
+ Headers: map[string]*string{
+ "x-amz-version-id": &versid,
+ },
+ MetaOpts: &MetaOptions{
+ BucketOwner: parsedAcl.Owner,
+ ObjectETag: res.ETag,
+ EventName: s3event.EventCompleteMultipartUpload,
+ VersionId: &versid,
+ },
+ }, err
+}
diff --git a/s3api/controllers/object-post_test.go b/s3api/controllers/object-post_test.go
new file mode 100644
index 00000000..3d96d7de
--- /dev/null
+++ b/s3api/controllers/object-post_test.go
@@ -0,0 +1,561 @@
+// Copyright 2023 Versity Software
+// This file is licensed under the Apache License, Version 2.0
+// (the "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package controllers
+
+import (
+ "bufio"
+ "context"
+ "encoding/xml"
+ "testing"
+
+ "github.com/aws/aws-sdk-go-v2/service/s3"
+ "github.com/aws/aws-sdk-go-v2/service/s3/types"
+ "github.com/stretchr/testify/assert"
+ "github.com/versity/versitygw/s3api/utils"
+ "github.com/versity/versitygw/s3err"
+ "github.com/versity/versitygw/s3event"
+ "github.com/versity/versitygw/s3response"
+)
+
+func TestS3ApiController_RestoreObject(t *testing.T) {
+ validRestoreBody, err := xml.Marshal(types.RestoreRequest{
+ Description: utils.GetStringPtr("description"),
+ Type: types.RestoreRequestTypeSelect,
+ })
+ assert.NoError(t, err)
+
+ tests := []struct {
+ name string
+ input testInput
+ output testOutput
+ }{
+ {
+ name: "verify access fails",
+ input: testInput{
+ locals: accessDeniedLocals,
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ },
+ },
+ err: s3err.GetAPIError(s3err.ErrAccessDenied),
+ },
+ },
+ {
+ name: "invalid request body",
+ input: testInput{
+ locals: defaultLocals,
+ body: []byte("invalid_body"),
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ },
+ },
+ err: s3err.GetAPIError(s3err.ErrMalformedXML),
+ },
+ },
+ {
+ name: "backend returns error",
+ input: testInput{
+ locals: defaultLocals,
+ beErr: s3err.GetAPIError(s3err.ErrNoSuchBucket),
+ body: validRestoreBody,
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ EventName: s3event.EventObjectRestoreCompleted,
+ },
+ },
+ err: s3err.GetAPIError(s3err.ErrNoSuchBucket),
+ },
+ },
+ {
+ name: "successful response",
+ input: testInput{
+ locals: defaultLocals,
+ body: validRestoreBody,
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ EventName: s3event.EventObjectRestoreCompleted,
+ },
+ },
+ },
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ be := &BackendMock{
+ RestoreObjectFunc: func(contextMoqParam context.Context, restoreObjectInput *s3.RestoreObjectInput) error {
+ return tt.input.beErr
+ },
+ GetBucketPolicyFunc: func(contextMoqParam context.Context, bucket string) ([]byte, error) {
+ return nil, s3err.GetAPIError(s3err.ErrAccessDenied)
+ },
+ }
+
+ ctrl := S3ApiController{
+ be: be,
+ }
+
+ testController(
+ t,
+ ctrl.RestoreObject,
+ tt.output.response,
+ tt.output.err,
+ ctxInputs{
+ locals: tt.input.locals,
+ body: tt.input.body,
+ })
+ })
+ }
+}
+
+func TestS3ApiController_SelectObjectContent(t *testing.T) {
+ validSelectBody, err := xml.Marshal(s3response.SelectObjectContentPayload{
+ Expression: utils.GetStringPtr("expression"),
+ ExpressionType: types.ExpressionTypeSql,
+ })
+ assert.NoError(t, err)
+
+ tests := []struct {
+ name string
+ input testInput
+ output testOutput
+ }{
+ {
+ name: "verify access fails",
+ input: testInput{
+ locals: accessDeniedLocals,
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ },
+ },
+ err: s3err.GetAPIError(s3err.ErrAccessDenied),
+ },
+ },
+ {
+ name: "invalid request body",
+ input: testInput{
+ locals: defaultLocals,
+ body: []byte("invalid_body"),
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ },
+ },
+ err: s3err.GetAPIError(s3err.ErrMalformedXML),
+ },
+ },
+ {
+ name: "successful response",
+ input: testInput{
+ locals: defaultLocals,
+ body: validSelectBody,
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ },
+ },
+ },
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ be := &BackendMock{
+ SelectObjectContentFunc: func(ctx context.Context, input *s3.SelectObjectContentInput) func(w *bufio.Writer) {
+ return func(w *bufio.Writer) {}
+ },
+ GetBucketPolicyFunc: func(contextMoqParam context.Context, bucket string) ([]byte, error) {
+ return nil, s3err.GetAPIError(s3err.ErrAccessDenied)
+ },
+ }
+
+ ctrl := S3ApiController{
+ be: be,
+ }
+
+ testController(
+ t,
+ ctrl.SelectObjectContent,
+ tt.output.response,
+ tt.output.err,
+ ctxInputs{
+ locals: tt.input.locals,
+ body: tt.input.body,
+ })
+ })
+ }
+}
+
+func TestS3ApiController_CreateMultipartUpload(t *testing.T) {
+ tests := []struct {
+ name string
+ input testInput
+ output testOutput
+ }{
+ {
+ name: "verify access fails",
+ input: testInput{
+ locals: accessDeniedLocals,
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ },
+ },
+ err: s3err.GetAPIError(s3err.ErrAccessDenied),
+ },
+ },
+ {
+ name: "invalid object lock headers",
+ input: testInput{
+ locals: defaultLocals,
+ headers: map[string]string{
+ "X-Amz-Object-Lock-Mode": string(types.ObjectLockModeGovernance),
+ },
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ },
+ },
+ err: s3err.GetAPIError(s3err.ErrObjectLockInvalidHeaders),
+ },
+ },
+ {
+ name: "invalid checksum headers",
+ input: testInput{
+ locals: defaultLocals,
+ headers: map[string]string{
+ "X-Amz-Checksum-Algorithm": "invalid_checksum_algo",
+ },
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ },
+ },
+ err: s3err.GetAPIError(s3err.ErrInvalidChecksumAlgorithm),
+ },
+ },
+ {
+ name: "backend returns error",
+ input: testInput{
+ locals: defaultLocals,
+ beErr: s3err.GetAPIError(s3err.ErrNoSuchBucket),
+ beRes: s3response.InitiateMultipartUploadResult{},
+ },
+ output: testOutput{
+ response: &Response{
+ Data: s3response.InitiateMultipartUploadResult{},
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ },
+ },
+ err: s3err.GetAPIError(s3err.ErrNoSuchBucket),
+ },
+ },
+ {
+ name: "successful response",
+ input: testInput{
+ locals: defaultLocals,
+ beRes: s3response.InitiateMultipartUploadResult{},
+ headers: map[string]string{
+ "x-amz-checksum-algorithm": string(types.ChecksumAlgorithmCrc32),
+ },
+ },
+ output: testOutput{
+ response: &Response{
+ Data: s3response.InitiateMultipartUploadResult{},
+ Headers: map[string]*string{
+ "x-amz-checksum-algorithm": utils.ConvertToStringPtr(types.ChecksumAlgorithmCrc32),
+ },
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ },
+ },
+ },
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ be := &BackendMock{
+ CreateMultipartUploadFunc: func(contextMoqParam context.Context, createMultipartUploadInput s3response.CreateMultipartUploadInput) (s3response.InitiateMultipartUploadResult, error) {
+ return tt.input.beRes.(s3response.InitiateMultipartUploadResult), tt.input.beErr
+ },
+ GetBucketPolicyFunc: func(contextMoqParam context.Context, bucket string) ([]byte, error) {
+ return nil, s3err.GetAPIError(s3err.ErrAccessDenied)
+ },
+ }
+
+ ctrl := S3ApiController{
+ be: be,
+ }
+
+ testController(
+ t,
+ ctrl.CreateMultipartUpload,
+ tt.output.response,
+ tt.output.err,
+ ctxInputs{
+ locals: tt.input.locals,
+ body: tt.input.body,
+ headers: tt.input.headers,
+ })
+ })
+ }
+}
+
+func TestS3ApiController_CompleteMultipartUpload(t *testing.T) {
+ emptyMpPartsBody, err := xml.Marshal(s3response.CompleteMultipartUploadRequestBody{
+ Parts: []types.CompletedPart{},
+ })
+ assert.NoError(t, err)
+ pn := int32(1)
+
+ validMpBody, err := xml.Marshal(s3response.CompleteMultipartUploadRequestBody{
+ Parts: []types.CompletedPart{
+ {
+ PartNumber: &pn,
+ ETag: utils.GetStringPtr("ETag"),
+ },
+ },
+ })
+ assert.NoError(t, err)
+
+ versionId, ETag := "versionId", "mock-ETag"
+
+ tests := []struct {
+ name string
+ input testInput
+ output testOutput
+ }{
+ {
+ name: "verify access fails",
+ input: testInput{
+ locals: accessDeniedLocals,
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ },
+ },
+ err: s3err.GetAPIError(s3err.ErrAccessDenied),
+ },
+ },
+ {
+ name: "invalid request body",
+ input: testInput{
+ locals: defaultLocals,
+ body: []byte("invalid_body"),
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ },
+ },
+ err: s3err.GetAPIError(s3err.ErrMalformedXML),
+ },
+ },
+ {
+ name: "request body empty mp parts",
+ input: testInput{
+ locals: defaultLocals,
+ body: emptyMpPartsBody,
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ },
+ },
+ err: s3err.GetAPIError(s3err.ErrEmptyParts),
+ },
+ },
+ {
+ name: "invalid mp parts header string",
+ input: testInput{
+ locals: defaultLocals,
+ body: validMpBody,
+ headers: map[string]string{
+ "X-Amz-Mp-Object-Size": "invalid_mp_object_size",
+ },
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ },
+ },
+ err: s3err.GetInvalidMpObjectSizeErr("invalid_mp_object_size"),
+ },
+ },
+ {
+ name: "negative mp parts header value",
+ input: testInput{
+ locals: defaultLocals,
+ body: validMpBody,
+ headers: map[string]string{
+ "X-Amz-Mp-Object-Size": "-4",
+ },
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ },
+ },
+ err: s3err.GetNegatvieMpObjectSizeErr(-4),
+ },
+ },
+ {
+ name: "invalid checksum headers",
+ input: testInput{
+ locals: defaultLocals,
+ body: validMpBody,
+ headers: map[string]string{
+ "X-Amz-Sdk-Checksum-Algorithm": "invalid_checksum_algo",
+ },
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ },
+ },
+ err: s3err.GetAPIError(s3err.ErrInvalidChecksumAlgorithm),
+ },
+ },
+ {
+ name: "invalid checksum type",
+ input: testInput{
+ locals: defaultLocals,
+ body: validMpBody,
+ headers: map[string]string{
+ "X-Amz-Checksum-Type": "invalid_checksum_type",
+ },
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ },
+ },
+ err: s3err.GetInvalidChecksumHeaderErr("x-amz-checksum-type"),
+ },
+ },
+ {
+ name: "backend returns error",
+ input: testInput{
+ locals: defaultLocals,
+ body: validMpBody,
+ beErr: s3err.GetAPIError(s3err.ErrNoSuchBucket),
+ beRes: s3response.CompleteMultipartUploadResult{},
+ },
+ output: testOutput{
+ response: &Response{
+ Data: s3response.CompleteMultipartUploadResult{},
+ Headers: map[string]*string{
+ "x-amz-version-id": &versionId,
+ },
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ EventName: s3event.EventCompleteMultipartUpload,
+ VersionId: &versionId,
+ ObjectETag: nil,
+ },
+ },
+ err: s3err.GetAPIError(s3err.ErrNoSuchBucket),
+ },
+ },
+ {
+ name: "successful response",
+ input: testInput{
+ locals: defaultLocals,
+ body: validMpBody,
+ beRes: s3response.CompleteMultipartUploadResult{
+ ETag: &ETag,
+ },
+ headers: map[string]string{
+ "X-Amz-Mp-Object-Size": "3",
+ },
+ },
+ output: testOutput{
+ response: &Response{
+ Data: s3response.CompleteMultipartUploadResult{
+ ETag: &ETag,
+ },
+ Headers: map[string]*string{
+ "x-amz-version-id": &versionId,
+ },
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ EventName: s3event.EventCompleteMultipartUpload,
+ VersionId: &versionId,
+ ObjectETag: &ETag,
+ },
+ },
+ },
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ be := &BackendMock{
+ CompleteMultipartUploadFunc: func(contextMoqParam context.Context, completeMultipartUploadInput *s3.CompleteMultipartUploadInput) (s3response.CompleteMultipartUploadResult, string, error) {
+ return tt.input.beRes.(s3response.CompleteMultipartUploadResult), versionId, tt.input.beErr
+ },
+ GetBucketPolicyFunc: func(contextMoqParam context.Context, bucket string) ([]byte, error) {
+ return nil, s3err.GetAPIError(s3err.ErrAccessDenied)
+ },
+ }
+
+ ctrl := S3ApiController{
+ be: be,
+ }
+
+ testController(
+ t,
+ ctrl.CompleteMultipartUpload,
+ tt.output.response,
+ tt.output.err,
+ ctxInputs{
+ locals: tt.input.locals,
+ body: tt.input.body,
+ headers: tt.input.headers,
+ })
+ })
+ }
+}
diff --git a/s3api/controllers/object-put.go b/s3api/controllers/object-put.go
new file mode 100644
index 00000000..77397d95
--- /dev/null
+++ b/s3api/controllers/object-put.go
@@ -0,0 +1,740 @@
+// Copyright 2023 Versity Software
+// This file is licensed under the Apache License, Version 2.0
+// (the "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package controllers
+
+import (
+ "bytes"
+ "encoding/xml"
+ "fmt"
+ "io"
+ "net/url"
+ "strconv"
+ "strings"
+ "time"
+
+ "github.com/aws/aws-sdk-go-v2/service/s3"
+ "github.com/aws/aws-sdk-go-v2/service/s3/types"
+ "github.com/gofiber/fiber/v2"
+ "github.com/versity/versitygw/auth"
+ "github.com/versity/versitygw/s3api/debuglogger"
+ "github.com/versity/versitygw/s3api/utils"
+ "github.com/versity/versitygw/s3err"
+ "github.com/versity/versitygw/s3event"
+ "github.com/versity/versitygw/s3response"
+)
+
+func (c S3ApiController) PutObjectTagging(ctx *fiber.Ctx) (*Response, error) {
+ bucket := ctx.Params("bucket")
+ key := strings.TrimPrefix(ctx.Path(), fmt.Sprintf("/%s/", bucket))
+ acct := utils.ContextKeyAccount.Get(ctx).(auth.Account)
+ isRoot := utils.ContextKeyIsRoot.Get(ctx).(bool)
+ IsBucketPublic := utils.ContextKeyPublicBucket.IsSet(ctx)
+ 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,
+ })
+ if err != nil {
+ return &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: parsedAcl.Owner,
+ },
+ }, err
+ }
+
+ tagging, err := utils.ParseTagging(ctx.Body(), utils.TagLimitObject)
+ if err != nil {
+ return &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: parsedAcl.Owner,
+ },
+ }, err
+ }
+
+ err = c.be.PutObjectTagging(ctx.Context(), bucket, key, tagging)
+ return &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: parsedAcl.Owner,
+ EventName: s3event.EventObjectTaggingPut,
+ },
+ }, err
+}
+
+func (c S3ApiController) PutObjectRetention(ctx *fiber.Ctx) (*Response, error) {
+ bucket := ctx.Params("bucket")
+ key := strings.TrimPrefix(ctx.Path(), fmt.Sprintf("/%s/", bucket))
+ versionId := ctx.Query("versionId")
+ bypass := strings.EqualFold(ctx.Get("X-Amz-Bypass-Governance-Retention"), "true")
+ acct := utils.ContextKeyAccount.Get(ctx).(auth.Account)
+ isRoot := utils.ContextKeyIsRoot.Get(ctx).(bool)
+ IsBucketPublic := utils.ContextKeyPublicBucket.IsSet(ctx)
+ 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,
+ }); err != nil {
+ return &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: parsedAcl.Owner,
+ },
+ }, err
+ }
+
+ if bypass {
+ policy, err := c.be.GetBucketPolicy(ctx.Context(), bucket)
+ if err != nil {
+ bypass = false
+ } else {
+ if err := auth.VerifyBucketPolicy(policy, acct.Access, bucket, key, auth.BypassGovernanceRetentionAction); err != nil {
+ bypass = false
+ }
+ }
+ }
+
+ retention, err := auth.ParseObjectLockRetentionInput(ctx.Body())
+ if err != nil {
+ debuglogger.Logf("failed to parse object lock configuration input: %v", err)
+ return &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: parsedAcl.Owner,
+ },
+ }, err
+ }
+
+ err = c.be.PutObjectRetention(ctx.Context(), bucket, key, versionId, bypass, retention)
+ return &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: parsedAcl.Owner,
+ },
+ }, err
+}
+
+func (c S3ApiController) PutObjectLegalHold(ctx *fiber.Ctx) (*Response, error) {
+ bucket := ctx.Params("bucket")
+ key := strings.TrimPrefix(ctx.Path(), fmt.Sprintf("/%s/", bucket))
+ versionId := ctx.Query("versionId")
+ acct := utils.ContextKeyAccount.Get(ctx).(auth.Account)
+ isRoot := utils.ContextKeyIsRoot.Get(ctx).(bool)
+ IsBucketPublic := utils.ContextKeyPublicBucket.IsSet(ctx)
+ 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,
+ }); err != nil {
+ return &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: parsedAcl.Owner,
+ },
+ }, err
+ }
+
+ var legalHold types.ObjectLockLegalHold
+ if err := xml.Unmarshal(ctx.Body(), &legalHold); err != nil {
+ debuglogger.Logf("failed to parse request body: %v", err)
+ return &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: parsedAcl.Owner,
+ },
+ }, s3err.GetAPIError(s3err.ErrMalformedXML)
+ }
+
+ if legalHold.Status != types.ObjectLockLegalHoldStatusOff && legalHold.Status != types.ObjectLockLegalHoldStatusOn {
+ debuglogger.Logf("invalid legal hold status: %v", legalHold.Status)
+ return &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: parsedAcl.Owner,
+ },
+ }, s3err.GetAPIError(s3err.ErrMalformedXML)
+ }
+
+ err := c.be.PutObjectLegalHold(ctx.Context(), bucket, key, versionId, legalHold.Status == types.ObjectLockLegalHoldStatusOn)
+ return &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: parsedAcl.Owner,
+ },
+ }, err
+}
+
+func (c S3ApiController) UploadPart(ctx *fiber.Ctx) (*Response, error) {
+ bucket := ctx.Params("bucket")
+ key := strings.TrimPrefix(ctx.Path(), fmt.Sprintf("/%s/", bucket))
+ partNumber := int32(ctx.QueryInt("partNumber", -1))
+ uploadId := ctx.Query("uploadId")
+ // context locals
+ acct := utils.ContextKeyAccount.Get(ctx).(auth.Account)
+ isRoot := utils.ContextKeyIsRoot.Get(ctx).(bool)
+ IsBucketPublic := utils.ContextKeyPublicBucket.IsSet(ctx)
+ parsedAcl := utils.ContextKeyParsedAcl.Get(ctx).(auth.ACL)
+
+ contentLengthStr := ctx.Get("Content-Length")
+ if contentLengthStr == "" {
+ contentLengthStr = "0"
+ }
+ // Use decoded content length if available because the
+ // middleware will decode the chunked transfer encoding
+ decodedLength := ctx.Get("X-Amz-Decoded-Content-Length")
+ if decodedLength != "" {
+ contentLengthStr = decodedLength
+ }
+
+ 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,
+ })
+ if err != nil {
+ return &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: parsedAcl.Owner,
+ },
+ }, err
+ }
+
+ if partNumber < minPartNumber || partNumber > maxPartNumber {
+ debuglogger.Logf("invalid part number: %d", partNumber)
+ return &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: parsedAcl.Owner,
+ },
+ }, s3err.GetAPIError(s3err.ErrInvalidPartNumber)
+ }
+
+ contentLength, err := strconv.ParseInt(contentLengthStr, 10, 64)
+ if err != nil {
+ debuglogger.Logf("error parsing content length %q: %v", contentLengthStr, err)
+ return &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: parsedAcl.Owner,
+ },
+ }, s3err.GetAPIError(s3err.ErrInvalidRequest)
+ }
+
+ algorithm, checksums, err := utils.ParseChecksumHeaders(ctx)
+ if err != nil {
+ debuglogger.Logf("err parsing checksum headers: %v", err)
+ return &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: parsedAcl.Owner,
+ },
+ }, err
+ }
+
+ var body io.Reader
+ bodyi := utils.ContextKeyBodyReader.Get(ctx)
+ if bodyi != nil {
+ body = bodyi.(io.Reader)
+ } else {
+ body = bytes.NewReader([]byte{})
+ }
+
+ res, err := c.be.UploadPart(ctx.Context(),
+ &s3.UploadPartInput{
+ Bucket: &bucket,
+ Key: &key,
+ UploadId: &uploadId,
+ PartNumber: &partNumber,
+ ContentLength: &contentLength,
+ Body: body,
+ ChecksumAlgorithm: algorithm,
+ ChecksumCRC32: utils.GetStringPtr(checksums[types.ChecksumAlgorithmCrc32]),
+ ChecksumCRC32C: utils.GetStringPtr(checksums[types.ChecksumAlgorithmCrc32c]),
+ ChecksumSHA1: utils.GetStringPtr(checksums[types.ChecksumAlgorithmSha1]),
+ ChecksumSHA256: utils.GetStringPtr(checksums[types.ChecksumAlgorithmSha256]),
+ ChecksumCRC64NVME: utils.GetStringPtr(checksums[types.ChecksumAlgorithmCrc64nvme]),
+ })
+ var headers map[string]*string
+ if err == nil {
+ headers = map[string]*string{
+ "ETag": res.ETag,
+ "x-amz-checksum-crc32": res.ChecksumCRC32,
+ "x-amz-checksum-crc32c": res.ChecksumCRC32C,
+ "x-amz-checksum-crc64nvme": res.ChecksumCRC64NVME,
+ "x-amz-checksum-sha1": res.ChecksumSHA1,
+ "x-amz-checksum-sha256": res.ChecksumSHA256,
+ }
+ }
+ return &Response{
+ Headers: headers,
+ MetaOpts: &MetaOptions{
+ ContentLength: contentLength,
+ BucketOwner: parsedAcl.Owner,
+ },
+ }, err
+
+}
+
+func (c S3ApiController) UploadPartCopy(ctx *fiber.Ctx) (*Response, error) {
+ bucket := ctx.Params("bucket")
+ key := strings.TrimPrefix(ctx.Path(), fmt.Sprintf("/%s/", bucket))
+ copySource := strings.TrimPrefix(ctx.Get("X-Amz-Copy-Source"), "/")
+ copySrcRange := ctx.Get("X-Amz-Copy-Source-Range")
+ partNumber := int32(ctx.QueryInt("partNumber", -1))
+ uploadId := ctx.Query("uploadId")
+ // context locals
+ acct := utils.ContextKeyAccount.Get(ctx).(auth.Account)
+ isRoot := utils.ContextKeyIsRoot.Get(ctx).(bool)
+ IsBucketPublic := utils.ContextKeyPublicBucket.IsSet(ctx)
+ parsedAcl := utils.ContextKeyParsedAcl.Get(ctx).(auth.ACL)
+
+ cs := copySource
+ copySource, err := url.QueryUnescape(copySource)
+ if err != nil {
+ debuglogger.Logf("error unescaping copy source %q: %v", cs, err)
+ return &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: parsedAcl.Owner,
+ },
+ }, s3err.GetAPIError(s3err.ErrInvalidCopySource)
+ }
+
+ 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,
+ })
+ if err != nil {
+ return &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: parsedAcl.Owner,
+ },
+ }, err
+ }
+
+ if partNumber < minPartNumber || partNumber > maxPartNumber {
+ debuglogger.Logf("invalid part number: %d", partNumber)
+ return &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: parsedAcl.Owner,
+ },
+ }, s3err.GetAPIError(s3err.ErrInvalidPartNumber)
+ }
+
+ resp, err := c.be.UploadPartCopy(ctx.Context(),
+ &s3.UploadPartCopyInput{
+ Bucket: &bucket,
+ Key: &key,
+ CopySource: ©Source,
+ PartNumber: &partNumber,
+ UploadId: &uploadId,
+ CopySourceRange: ©SrcRange,
+ })
+ var headers map[string]*string
+ if err == nil && resp.CopySourceVersionId != "" {
+ headers = map[string]*string{
+ "x-amz-copy-source-version-id": &resp.CopySourceVersionId,
+ }
+ }
+ return &Response{
+ Headers: headers,
+ Data: resp,
+ MetaOpts: &MetaOptions{
+ BucketOwner: parsedAcl.Owner,
+ },
+ }, err
+}
+
+func (c S3ApiController) PutObjectAcl(ctx *fiber.Ctx) (*Response, error) {
+ bucket := ctx.Params("bucket")
+ key := strings.TrimPrefix(ctx.Path(), fmt.Sprintf("/%s/", bucket))
+ acl := ctx.Get("X-Amz-Acl")
+ grantFullControl := ctx.Get("X-Amz-Grant-Full-Control")
+ grantRead := ctx.Get("X-Amz-Grant-Read")
+ grantReadACP := ctx.Get("X-Amz-Grant-Read-Acp")
+ grantWrite := ctx.Get("X-Amz-Grant-Write")
+ grantWriteACP := ctx.Get("X-Amz-Grant-Write-Acp")
+ // context locals
+ acct := utils.ContextKeyAccount.Get(ctx).(auth.Account)
+ isRoot := utils.ContextKeyIsRoot.Get(ctx).(bool)
+ 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.PutObjectAclAction,
+ })
+ if err != nil {
+ return &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: parsedAcl.Owner,
+ },
+ }, err
+ }
+
+ err = c.be.PutObjectAcl(ctx.Context(), &s3.PutObjectAclInput{
+ Bucket: &bucket,
+ Key: &key,
+ GrantFullControl: &grantFullControl,
+ GrantRead: &grantRead,
+ GrantWrite: &grantWrite,
+ ACL: types.ObjectCannedACL(acl),
+ GrantReadACP: &grantReadACP,
+ GrantWriteACP: &grantWriteACP,
+ })
+ return &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: parsedAcl.Owner,
+ EventName: s3event.EventObjectAclPut,
+ },
+ }, err
+}
+
+func (c S3ApiController) CopyObject(ctx *fiber.Ctx) (*Response, error) {
+ bucket := ctx.Params("bucket")
+ key := strings.TrimPrefix(ctx.Path(), fmt.Sprintf("/%s/", bucket))
+ copySource := strings.TrimPrefix(ctx.Get("X-Amz-Copy-Source"), "/")
+ copySrcIfMatch := ctx.Get("X-Amz-Copy-Source-If-Match")
+ copySrcIfNoneMatch := ctx.Get("X-Amz-Copy-Source-If-None-Match")
+ copySrcModifSince := ctx.Get("X-Amz-Copy-Source-If-Modified-Since")
+ copySrcUnmodifSince := ctx.Get("X-Amz-Copy-Source-If-Unmodified-Since")
+ metaDirective := types.MetadataDirective(ctx.Get("X-Amz-Metadata-Directive", string(types.MetadataDirectiveCopy)))
+ taggingDirective := types.TaggingDirective(ctx.Get("X-Amz-Tagging-Directive", string(types.TaggingDirectiveCopy)))
+ contentType := ctx.Get("Content-Type")
+ contentEncoding := ctx.Get("Content-Encoding")
+ contentDisposition := ctx.Get("Content-Disposition")
+ contentLanguage := ctx.Get("Content-Language")
+ cacheControl := ctx.Get("Cache-Control")
+ expires := ctx.Get("Expires")
+ tagging := ctx.Get("x-amz-tagging")
+ storageClass := ctx.Get("X-Amz-Storage-Class")
+ // context locals
+ acct := utils.ContextKeyAccount.Get(ctx).(auth.Account)
+ isRoot := utils.ContextKeyIsRoot.Get(ctx).(bool)
+ parsedAcl := utils.ContextKeyParsedAcl.Get(ctx).(auth.ACL)
+
+ cs := copySource
+ copySource, err := url.QueryUnescape(copySource)
+ if err != nil {
+ debuglogger.Logf("error unescaping copy source %q: %v", cs, err)
+ return &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: parsedAcl.Owner,
+ },
+ }, s3err.GetAPIError(s3err.ErrInvalidCopySource)
+ }
+
+ 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,
+ })
+ if err != nil {
+ return &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: parsedAcl.Owner,
+ },
+ }, err
+ }
+
+ var mtime *time.Time
+ if copySrcModifSince != "" {
+ tm, err := time.Parse(iso8601Format, copySrcModifSince)
+ if err != nil {
+ debuglogger.Logf("error parsing copy source modified since %q: %v", copySrcModifSince, err)
+ return &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: parsedAcl.Owner,
+ },
+ }, s3err.GetAPIError(s3err.ErrInvalidCopySource)
+ }
+ mtime = &tm
+ }
+ var umtime *time.Time
+ if copySrcUnmodifSince != "" {
+ tm, err := time.Parse(iso8601Format, copySrcUnmodifSince)
+ if err != nil {
+ debuglogger.Logf("error parsing copy source unmodified since %q: %v", copySrcUnmodifSince, err)
+ return &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: parsedAcl.Owner,
+ },
+ }, s3err.GetAPIError(s3err.ErrInvalidCopySource)
+ }
+ umtime = &tm
+ }
+
+ metadata := utils.GetUserMetaData(&ctx.Request().Header)
+
+ if metaDirective != "" && metaDirective != types.MetadataDirectiveCopy && metaDirective != types.MetadataDirectiveReplace {
+ debuglogger.Logf("invalid metadata directive: %v", metaDirective)
+ return &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: parsedAcl.Owner,
+ },
+ }, s3err.GetAPIError(s3err.ErrInvalidMetadataDirective)
+ }
+
+ if taggingDirective != "" && taggingDirective != types.TaggingDirectiveCopy && taggingDirective != types.TaggingDirectiveReplace {
+ debuglogger.Logf("invalid tagging directive: %v", taggingDirective)
+ return &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: parsedAcl.Owner,
+ },
+ }, s3err.GetAPIError(s3err.ErrInvalidTaggingDirective)
+ }
+
+ checksumAlgorithm := types.ChecksumAlgorithm(ctx.Get("x-amz-checksum-algorithm"))
+ err = utils.IsChecksumAlgorithmValid(checksumAlgorithm)
+ if err != nil {
+ return &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: parsedAcl.Owner,
+ },
+ }, err
+ }
+
+ objLock, err := utils.ParsObjectLockHdrs(ctx)
+ if err != nil {
+ return &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: parsedAcl.Owner,
+ },
+ }, err
+ }
+
+ res, err := c.be.CopyObject(ctx.Context(),
+ s3response.CopyObjectInput{
+ Bucket: &bucket,
+ Key: &key,
+ ContentType: &contentType,
+ ContentDisposition: &contentDisposition,
+ ContentEncoding: &contentEncoding,
+ ContentLanguage: &contentLanguage,
+ CacheControl: &cacheControl,
+ Expires: &expires,
+ Tagging: &tagging,
+ TaggingDirective: taggingDirective,
+ CopySource: ©Source,
+ CopySourceIfMatch: ©SrcIfMatch,
+ CopySourceIfNoneMatch: ©SrcIfNoneMatch,
+ CopySourceIfModifiedSince: mtime,
+ CopySourceIfUnmodifiedSince: umtime,
+ ExpectedBucketOwner: &acct.Access,
+ Metadata: metadata,
+ MetadataDirective: metaDirective,
+ StorageClass: types.StorageClass(storageClass),
+ ChecksumAlgorithm: checksumAlgorithm,
+ ObjectLockRetainUntilDate: &objLock.RetainUntilDate,
+ ObjectLockLegalHoldStatus: objLock.LegalHoldStatus,
+ ObjectLockMode: objLock.ObjectLockMode,
+ })
+
+ var etag *string
+ if err == nil {
+ etag = res.CopyObjectResult.ETag
+ }
+
+ return &Response{
+ Headers: map[string]*string{
+ "x-amz-version-id": res.VersionId,
+ "x-amz-copy-source-version-id": res.CopySourceVersionId,
+ },
+ Data: res.CopyObjectResult,
+ MetaOpts: &MetaOptions{
+ BucketOwner: parsedAcl.Owner,
+ ObjectETag: etag,
+ VersionId: res.VersionId,
+ EventName: s3event.EventObjectCreatedCopy,
+ },
+ }, err
+}
+
+func (c S3ApiController) PutObject(ctx *fiber.Ctx) (*Response, error) {
+ bucket := ctx.Params("bucket")
+ key := strings.TrimPrefix(ctx.Path(), fmt.Sprintf("/%s/", bucket))
+ contentType := ctx.Get("Content-Type")
+ contentEncoding := ctx.Get("Content-Encoding")
+ contentDisposition := ctx.Get("Content-Disposition")
+ contentLanguage := ctx.Get("Content-Language")
+ cacheControl := ctx.Get("Cache-Control")
+ expires := ctx.Get("Expires")
+ tagging := ctx.Get("x-amz-tagging")
+ // context locals
+ acct := utils.ContextKeyAccount.Get(ctx).(auth.Account)
+ isRoot := utils.ContextKeyIsRoot.Get(ctx).(bool)
+ parsedAcl := utils.ContextKeyParsedAcl.Get(ctx).(auth.ACL)
+ IsBucketPublic := utils.ContextKeyPublicBucket.IsSet(ctx)
+
+ // Content Length
+ contentLengthStr := ctx.Get("Content-Length")
+ if contentLengthStr == "" {
+ contentLengthStr = "0"
+ }
+ // Use decoded content length if available because the
+ // middleware will decode the chunked transfer encoding
+ decodedLength := ctx.Get("X-Amz-Decoded-Content-Length")
+ if decodedLength != "" {
+ contentLengthStr = decodedLength
+ }
+
+ // load the meta headers
+ metadata := utils.GetUserMetaData(&ctx.Request().Header)
+
+ 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,
+ })
+ if err != nil {
+ return &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: parsedAcl.Owner,
+ },
+ }, err
+ }
+
+ err = auth.CheckObjectAccess(ctx.Context(), bucket, acct.Access, []types.ObjectIdentifier{{Key: &key}}, true, IsBucketPublic, c.be)
+ if err != nil {
+ return &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: parsedAcl.Owner,
+ },
+ }, err
+ }
+
+ contentLength, err := strconv.ParseInt(contentLengthStr, 10, 64)
+ if err != nil {
+ debuglogger.Logf("error parsing content length %q: %v", contentLengthStr, err)
+ return &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: parsedAcl.Owner,
+ },
+ }, s3err.GetAPIError(s3err.ErrInvalidRequest)
+ }
+
+ objLock, err := utils.ParsObjectLockHdrs(ctx)
+ if err != nil {
+ return &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: parsedAcl.Owner,
+ },
+ }, err
+ }
+
+ algorithm, checksums, err := utils.ParseChecksumHeaders(ctx)
+ if err != nil {
+ return &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: parsedAcl.Owner,
+ },
+ }, err
+ }
+
+ var body io.Reader
+ bodyi := utils.ContextKeyBodyReader.Get(ctx)
+ if bodyi != nil {
+ body = bodyi.(io.Reader)
+ } else {
+ body = bytes.NewReader([]byte{})
+ }
+
+ res, err := c.be.PutObject(ctx.Context(),
+ s3response.PutObjectInput{
+ Bucket: &bucket,
+ Key: &key,
+ ContentLength: &contentLength,
+ ContentType: &contentType,
+ ContentEncoding: &contentEncoding,
+ ContentDisposition: &contentDisposition,
+ ContentLanguage: &contentLanguage,
+ CacheControl: &cacheControl,
+ Expires: &expires,
+ Metadata: metadata,
+ Body: body,
+ Tagging: &tagging,
+ ObjectLockRetainUntilDate: &objLock.RetainUntilDate,
+ ObjectLockMode: objLock.ObjectLockMode,
+ ObjectLockLegalHoldStatus: objLock.LegalHoldStatus,
+ ChecksumAlgorithm: algorithm,
+ ChecksumCRC32: utils.GetStringPtr(checksums[types.ChecksumAlgorithmCrc32]),
+ ChecksumCRC32C: utils.GetStringPtr(checksums[types.ChecksumAlgorithmCrc32c]),
+ ChecksumSHA1: utils.GetStringPtr(checksums[types.ChecksumAlgorithmSha1]),
+ ChecksumSHA256: utils.GetStringPtr(checksums[types.ChecksumAlgorithmSha256]),
+ ChecksumCRC64NVME: utils.GetStringPtr(checksums[types.ChecksumAlgorithmCrc64nvme]),
+ })
+ return &Response{
+ Headers: map[string]*string{
+ "ETag": &res.ETag,
+ "x-amz-checksum-crc32": res.ChecksumCRC32,
+ "x-amz-checksum-crc32c": res.ChecksumCRC32C,
+ "x-amz-checksum-crc64nvme": res.ChecksumCRC64NVME,
+ "x-amz-checksum-sha1": res.ChecksumSHA1,
+ "x-amz-checksum-sha256": res.ChecksumSHA256,
+ "x-amz-checksum-type": utils.ConvertToStringPtr(res.ChecksumType),
+ "x-amz-version-id": &res.VersionID,
+ },
+ MetaOpts: &MetaOptions{
+ ContentLength: contentLength,
+ BucketOwner: parsedAcl.Owner,
+ ObjectETag: &res.ETag,
+ ObjectSize: contentLength,
+ EventName: s3event.EventObjectCreatedPut,
+ },
+ }, err
+}
diff --git a/s3api/controllers/object-put_test.go b/s3api/controllers/object-put_test.go
new file mode 100644
index 00000000..516f02dc
--- /dev/null
+++ b/s3api/controllers/object-put_test.go
@@ -0,0 +1,1243 @@
+// Copyright 2023 Versity Software
+// This file is licensed under the Apache License, Version 2.0
+// (the "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package controllers
+
+import (
+ "context"
+ "encoding/xml"
+ "strings"
+ "testing"
+ "time"
+
+ "github.com/aws/aws-sdk-go-v2/service/s3"
+ "github.com/aws/aws-sdk-go-v2/service/s3/types"
+ "github.com/stretchr/testify/assert"
+ "github.com/versity/versitygw/auth"
+ "github.com/versity/versitygw/s3api/utils"
+ "github.com/versity/versitygw/s3err"
+ "github.com/versity/versitygw/s3event"
+ "github.com/versity/versitygw/s3response"
+)
+
+func TestS3ApiController_PutObjectTagging(t *testing.T) {
+ validTaggingBody, err := xml.Marshal(
+ s3response.TaggingInput{
+ TagSet: s3response.TagSet{
+ Tags: []s3response.Tag{
+ {
+ Key: "key",
+ Value: "val",
+ },
+ },
+ },
+ })
+ assert.NoError(t, err)
+
+ tests := []struct {
+ name string
+ input testInput
+ output testOutput
+ }{
+ {
+ name: "verify access fails",
+ input: testInput{
+ locals: accessDeniedLocals,
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ },
+ },
+ err: s3err.GetAPIError(s3err.ErrAccessDenied),
+ },
+ },
+ {
+ name: "invalid request body",
+ input: testInput{
+ locals: defaultLocals,
+ body: []byte("invalid_body"),
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ },
+ },
+ err: s3err.GetAPIError(s3err.ErrMalformedXML),
+ },
+ },
+ {
+ name: "backend returns error",
+ input: testInput{
+ locals: defaultLocals,
+ beErr: s3err.GetAPIError(s3err.ErrNoSuchBucket),
+ body: validTaggingBody,
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ EventName: s3event.EventObjectTaggingPut,
+ },
+ },
+ err: s3err.GetAPIError(s3err.ErrNoSuchBucket),
+ },
+ },
+ {
+ name: "successful response",
+ input: testInput{
+ locals: defaultLocals,
+ body: validTaggingBody,
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ EventName: s3event.EventObjectTaggingPut,
+ },
+ },
+ },
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ be := &BackendMock{
+ PutObjectTaggingFunc: func(contextMoqParam context.Context, bucket, object string, tags map[string]string) error {
+ return tt.input.beErr
+ },
+ GetBucketPolicyFunc: func(contextMoqParam context.Context, bucket string) ([]byte, error) {
+ return nil, s3err.GetAPIError(s3err.ErrAccessDenied)
+ },
+ }
+
+ ctrl := S3ApiController{
+ be: be,
+ }
+
+ testController(
+ t,
+ ctrl.PutObjectTagging,
+ tt.output.response,
+ tt.output.err,
+ ctxInputs{
+ locals: tt.input.locals,
+ body: tt.input.body,
+ })
+ })
+ }
+}
+
+func TestS3ApiController_PutObjectRetention(t *testing.T) {
+ retDate := time.Now().Add(time.Hour * 3)
+ validRetentionBody, err := xml.Marshal(
+ s3response.PutObjectRetentionInput{
+ Mode: types.ObjectLockRetentionModeGovernance,
+ RetainUntilDate: s3response.AmzDate{
+ Time: retDate,
+ },
+ })
+ assert.NoError(t, err)
+
+ tests := []struct {
+ name string
+ input testInput
+ output testOutput
+ }{
+ {
+ name: "verify access fails",
+ input: testInput{
+ locals: accessDeniedLocals,
+ extraMockErr: s3err.GetAPIError(s3err.ErrAccessDenied),
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ },
+ },
+ err: s3err.GetAPIError(s3err.ErrAccessDenied),
+ },
+ },
+ {
+ name: "invalid request body",
+ input: testInput{
+ locals: defaultLocals,
+ body: []byte("invalid_body"),
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ },
+ },
+ err: s3err.GetAPIError(s3err.ErrMalformedXML),
+ },
+ },
+ {
+ name: "backend returns error",
+ input: testInput{
+ locals: defaultLocals,
+ beErr: s3err.GetAPIError(s3err.ErrNoSuchBucket),
+ body: validRetentionBody,
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ },
+ },
+ err: s3err.GetAPIError(s3err.ErrNoSuchBucket),
+ },
+ },
+ {
+ name: "success bypass GetBucketPolicy fails",
+ input: testInput{
+ locals: defaultLocals,
+ body: validRetentionBody,
+ extraMockErr: s3err.GetAPIError(s3err.ErrAccessDenied),
+ headers: map[string]string{
+ "X-Amz-Bypass-Governance-Retention": "true",
+ },
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ },
+ },
+ },
+ },
+ {
+ name: "success bypass VerifyBucketPolicy fails",
+ input: testInput{
+ locals: defaultLocals,
+ body: validRetentionBody,
+ extraMockResp: []byte("invalid_policy"),
+ headers: map[string]string{
+ "X-Amz-Bypass-Governance-Retention": "true",
+ },
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ },
+ },
+ },
+ },
+ {
+ name: "successful response",
+ input: testInput{
+ locals: defaultLocals,
+ body: validRetentionBody,
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ },
+ },
+ },
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ be := &BackendMock{
+ PutObjectRetentionFunc: func(contextMoqParam context.Context, bucket, object, versionId string, bypass bool, retention []byte) error {
+ return tt.input.beErr
+ },
+ GetBucketPolicyFunc: func(contextMoqParam context.Context, bucket string) ([]byte, error) {
+ if tt.input.extraMockResp == nil {
+ return nil, tt.input.extraMockErr
+ } else {
+ return tt.input.extraMockResp.([]byte), tt.input.extraMockErr
+ }
+ },
+ }
+
+ ctrl := S3ApiController{
+ be: be,
+ }
+
+ testController(
+ t,
+ ctrl.PutObjectRetention,
+ tt.output.response,
+ tt.output.err,
+ ctxInputs{
+ locals: tt.input.locals,
+ body: tt.input.body,
+ headers: tt.input.headers,
+ })
+ })
+ }
+}
+
+func TestS3ApiController_PutObjectLegalHold(t *testing.T) {
+ validBody, err := xml.Marshal(
+ types.ObjectLockLegalHold{
+ Status: types.ObjectLockLegalHoldStatusOn,
+ })
+ assert.NoError(t, err)
+ invalidStatusBody, err := xml.Marshal(
+ types.ObjectLockLegalHold{
+ Status: types.ObjectLockLegalHoldStatus("invalid_status"),
+ })
+ assert.NoError(t, err)
+
+ tests := []struct {
+ name string
+ input testInput
+ output testOutput
+ }{
+ {
+ name: "verify access fails",
+ input: testInput{
+ locals: accessDeniedLocals,
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ },
+ },
+ err: s3err.GetAPIError(s3err.ErrAccessDenied),
+ },
+ },
+ {
+ name: "invalid request body",
+ input: testInput{
+ locals: defaultLocals,
+ body: []byte("invalid_body"),
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ },
+ },
+ err: s3err.GetAPIError(s3err.ErrMalformedXML),
+ },
+ },
+ {
+ name: "invalid legal hold status",
+ input: testInput{
+ locals: defaultLocals,
+ body: invalidStatusBody,
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ },
+ },
+ err: s3err.GetAPIError(s3err.ErrMalformedXML),
+ },
+ },
+ {
+ name: "backend returns error",
+ input: testInput{
+ locals: defaultLocals,
+ beErr: s3err.GetAPIError(s3err.ErrNoSuchBucket),
+ body: validBody,
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ },
+ },
+ err: s3err.GetAPIError(s3err.ErrNoSuchBucket),
+ },
+ },
+ {
+ name: "successful response",
+ input: testInput{
+ locals: defaultLocals,
+ body: validBody,
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ },
+ },
+ },
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ be := &BackendMock{
+ PutObjectLegalHoldFunc: func(contextMoqParam context.Context, bucket, object, versionId string, status bool) error {
+ return tt.input.beErr
+ },
+ GetBucketPolicyFunc: func(contextMoqParam context.Context, bucket string) ([]byte, error) {
+ return nil, s3err.GetAPIError(s3err.ErrAccessDenied)
+ },
+ }
+
+ ctrl := S3ApiController{
+ be: be,
+ }
+
+ testController(
+ t,
+ ctrl.PutObjectLegalHold,
+ tt.output.response,
+ tt.output.err,
+ ctxInputs{
+ locals: tt.input.locals,
+ body: tt.input.body,
+ })
+ })
+ }
+}
+
+func TestS3ApiController_UploadPart(t *testing.T) {
+ tests := []struct {
+ name string
+ input testInput
+ output testOutput
+ }{
+ {
+ name: "verify access fails",
+ input: testInput{
+ locals: accessDeniedLocals,
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ },
+ },
+ err: s3err.GetAPIError(s3err.ErrAccessDenied),
+ },
+ },
+ {
+ name: "invalid part number",
+ input: testInput{
+ locals: defaultLocals,
+ queries: map[string]string{
+ "partNumber": "-2",
+ },
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ },
+ },
+ err: s3err.GetAPIError(s3err.ErrInvalidPartNumber),
+ },
+ },
+ {
+ name: "invalid content length",
+ input: testInput{
+ locals: defaultLocals,
+ headers: map[string]string{
+ "X-Amz-Decoded-Content-Length": "invalid_cLength",
+ },
+ queries: map[string]string{
+ "partNumber": "2",
+ },
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ },
+ },
+ err: s3err.GetAPIError(s3err.ErrInvalidRequest),
+ },
+ },
+ {
+ name: "invalid checksum header",
+ input: testInput{
+ locals: defaultLocals,
+ headers: map[string]string{
+ "X-Amz-Sdk-Checksum-Algorithm": "invalid_algo",
+ },
+ queries: map[string]string{
+ "partNumber": "2",
+ },
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ },
+ },
+ err: s3err.GetAPIError(s3err.ErrInvalidChecksumAlgorithm),
+ },
+ },
+ {
+ name: "backend returns error",
+ input: testInput{
+ locals: defaultLocals,
+ beErr: s3err.GetAPIError(s3err.ErrNoSuchBucket),
+ beRes: &s3.UploadPartOutput{},
+ queries: map[string]string{
+ "partNumber": "2",
+ },
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ },
+ },
+ err: s3err.GetAPIError(s3err.ErrNoSuchBucket),
+ },
+ },
+ {
+ name: "successful response",
+ input: testInput{
+ locals: map[utils.ContextKey]any{
+ utils.ContextKeyIsRoot: true,
+ utils.ContextKeyParsedAcl: auth.ACL{
+ Owner: "root",
+ },
+ utils.ContextKeyAccount: auth.Account{
+ Access: "root",
+ Role: auth.RoleAdmin,
+ },
+ utils.ContextKeyBodyReader: strings.NewReader("hello world"),
+ },
+ queries: map[string]string{
+ "partNumber": "2",
+ },
+ headers: map[string]string{
+ "Content-Length": "4",
+ },
+ body: []byte("bbbb"),
+ beRes: &s3.UploadPartOutput{
+ ETag: utils.GetStringPtr("ETag"),
+ },
+ },
+ output: testOutput{
+ response: &Response{
+ Headers: map[string]*string{
+ "ETag": utils.GetStringPtr("ETag"),
+ "x-amz-checksum-crc32": nil,
+ "x-amz-checksum-crc32c": nil,
+ "x-amz-checksum-crc64nvme": nil,
+ "x-amz-checksum-sha1": nil,
+ "x-amz-checksum-sha256": nil,
+ },
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ ContentLength: 4,
+ },
+ },
+ },
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ be := &BackendMock{
+ UploadPartFunc: func(contextMoqParam context.Context, uploadPartInput *s3.UploadPartInput) (*s3.UploadPartOutput, error) {
+ return tt.input.beRes.(*s3.UploadPartOutput), tt.input.beErr
+ },
+ GetBucketPolicyFunc: func(contextMoqParam context.Context, bucket string) ([]byte, error) {
+ return nil, s3err.GetAPIError(s3err.ErrAccessDenied)
+ },
+ }
+
+ ctrl := S3ApiController{
+ be: be,
+ }
+
+ testController(
+ t,
+ ctrl.UploadPart,
+ tt.output.response,
+ tt.output.err,
+ ctxInputs{
+ locals: tt.input.locals,
+ body: tt.input.body,
+ headers: tt.input.headers,
+ queries: tt.input.queries,
+ })
+ })
+ }
+}
+
+func TestS3ApiController_UploadPartCopy(t *testing.T) {
+ tests := []struct {
+ name string
+ input testInput
+ output testOutput
+ }{
+ {
+ name: "verify access fails",
+ input: testInput{
+ locals: accessDeniedLocals,
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ },
+ },
+ err: s3err.GetAPIError(s3err.ErrAccessDenied),
+ },
+ },
+ {
+ name: "invalid copy source",
+ input: testInput{
+ locals: defaultLocals,
+ headers: map[string]string{
+ "X-Amz-Copy-Source": "bad%G1",
+ },
+ queries: map[string]string{
+ "partNumber": "2",
+ },
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ },
+ },
+ err: s3err.GetAPIError(s3err.ErrInvalidCopySource),
+ },
+ },
+ {
+ name: "invalid part number",
+ input: testInput{
+ locals: defaultLocals,
+ headers: map[string]string{
+ "X-Amz-Copy-Source": "bucket/object",
+ },
+ queries: map[string]string{
+ "partNumber": "-2",
+ },
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ },
+ },
+ err: s3err.GetAPIError(s3err.ErrInvalidPartNumber),
+ },
+ },
+ {
+ name: "backend returns error",
+ input: testInput{
+ locals: defaultLocals,
+ beErr: s3err.GetAPIError(s3err.ErrNoSuchBucket),
+ beRes: s3response.CopyPartResult{},
+ queries: map[string]string{
+ "partNumber": "2",
+ },
+ headers: map[string]string{
+ "X-Amz-Copy-Source": "bucket/object",
+ },
+ },
+ output: testOutput{
+ response: &Response{
+ Data: s3response.CopyPartResult{},
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ },
+ },
+ err: s3err.GetAPIError(s3err.ErrNoSuchBucket),
+ },
+ },
+ {
+ name: "successful response",
+ input: testInput{
+ locals: defaultLocals,
+ queries: map[string]string{
+ "partNumber": "2",
+ },
+
+ headers: map[string]string{
+ "X-Amz-Copy-Source": "bucket/object",
+ },
+ beRes: s3response.CopyPartResult{
+ CopySourceVersionId: "versionId",
+ },
+ },
+ output: testOutput{
+ response: &Response{
+ Data: s3response.CopyPartResult{
+ CopySourceVersionId: "versionId",
+ },
+ Headers: map[string]*string{
+ "x-amz-copy-source-version-id": utils.GetStringPtr("versionId"),
+ },
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ },
+ },
+ },
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ be := &BackendMock{
+ UploadPartCopyFunc: func(contextMoqParam context.Context, uploadPartCopyInput *s3.UploadPartCopyInput) (s3response.CopyPartResult, error) {
+ return tt.input.beRes.(s3response.CopyPartResult), tt.input.beErr
+ },
+ GetBucketPolicyFunc: func(contextMoqParam context.Context, bucket string) ([]byte, error) {
+ return nil, s3err.GetAPIError(s3err.ErrAccessDenied)
+ },
+ }
+
+ ctrl := S3ApiController{
+ be: be,
+ }
+
+ testController(
+ t,
+ ctrl.UploadPartCopy,
+ tt.output.response,
+ tt.output.err,
+ ctxInputs{
+ locals: tt.input.locals,
+ headers: tt.input.headers,
+ queries: tt.input.queries,
+ })
+ })
+ }
+}
+
+func TestS3ApiController_PutObjectAcl(t *testing.T) {
+ tests := []struct {
+ name string
+ input testInput
+ output testOutput
+ }{
+ {
+ name: "verify access fails",
+ input: testInput{
+ locals: accessDeniedLocals,
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ },
+ },
+ err: s3err.GetAPIError(s3err.ErrAccessDenied),
+ },
+ },
+ {
+ name: "backend returns error",
+ input: testInput{
+ locals: defaultLocals,
+ beErr: s3err.GetAPIError(s3err.ErrNotImplemented),
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ EventName: s3event.EventObjectAclPut,
+ },
+ },
+ err: s3err.GetAPIError(s3err.ErrNotImplemented),
+ },
+ },
+ {
+ name: "successful response",
+ input: testInput{
+ locals: defaultLocals,
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ EventName: s3event.EventObjectAclPut,
+ },
+ },
+ },
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ be := &BackendMock{
+ PutObjectAclFunc: func(contextMoqParam context.Context, putObjectAclInput *s3.PutObjectAclInput) error {
+ return tt.input.beErr
+ },
+ GetBucketPolicyFunc: func(contextMoqParam context.Context, bucket string) ([]byte, error) {
+ return nil, s3err.GetAPIError(s3err.ErrAccessDenied)
+ },
+ }
+
+ ctrl := S3ApiController{
+ be: be,
+ }
+
+ testController(
+ t,
+ ctrl.PutObjectAcl,
+ tt.output.response,
+ tt.output.err,
+ ctxInputs{
+ locals: tt.input.locals,
+ })
+ })
+ }
+}
+
+func TestS3ApiController_CopyObject(t *testing.T) {
+ var nilResp *s3response.CopyObjectResult
+ tests := []struct {
+ name string
+ input testInput
+ output testOutput
+ }{
+ {
+ name: "verify access fails",
+ input: testInput{
+ locals: accessDeniedLocals,
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ },
+ },
+ err: s3err.GetAPIError(s3err.ErrAccessDenied),
+ },
+ },
+ {
+ name: "invalid copy source",
+ input: testInput{
+ locals: defaultLocals,
+ headers: map[string]string{
+ "X-Amz-Copy-Source": "bad%G1",
+ },
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ },
+ },
+ err: s3err.GetAPIError(s3err.ErrInvalidCopySource),
+ },
+ },
+ {
+ name: "invalid copy source modified since",
+ input: testInput{
+ locals: defaultLocals,
+ headers: map[string]string{
+ "X-Amz-Copy-Source": "bucket/object",
+ "X-Amz-Copy-Source-If-Modified-Since": "invalid_date",
+ },
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ },
+ },
+ err: s3err.GetAPIError(s3err.ErrInvalidCopySource),
+ },
+ },
+ {
+ name: "invalid copy source unmodified since",
+ input: testInput{
+ locals: defaultLocals,
+ headers: map[string]string{
+ "X-Amz-Copy-Source": "bucket/object",
+ "X-Amz-Copy-Source-If-Unmodified-Since": "invalid_date",
+ },
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ },
+ },
+ err: s3err.GetAPIError(s3err.ErrInvalidCopySource),
+ },
+ },
+ {
+ name: "invalid metadata directive",
+ input: testInput{
+ locals: defaultLocals,
+ headers: map[string]string{
+ "X-Amz-Copy-Source": "bucket/object",
+ "X-Amz-Copy-Source-If-Unmodified-Since": "20250102T150405Z",
+ "X-Amz-Copy-Source-If-Modified-Since": "20240102T150405Z",
+ "X-Amz-Metadata-Directive": "invalid_metadat_directive",
+ },
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ },
+ },
+ err: s3err.GetAPIError(s3err.ErrInvalidMetadataDirective),
+ },
+ },
+ {
+ name: "invalid tagging directive",
+ input: testInput{
+ locals: defaultLocals,
+ headers: map[string]string{
+ "X-Amz-Copy-Source": "bucket/object",
+ "X-Amz-Tagging-Directive": "invalid_tagging_directive",
+ },
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ },
+ },
+ err: s3err.GetAPIError(s3err.ErrInvalidTaggingDirective),
+ },
+ },
+ {
+ name: "invalid checksum algorithm",
+ input: testInput{
+ locals: defaultLocals,
+ headers: map[string]string{
+ "X-Amz-Copy-Source": "bucket/object",
+ "x-amz-checksum-algorithm": "invalid_checksum_algo",
+ },
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ },
+ },
+ err: s3err.GetAPIError(s3err.ErrInvalidChecksumAlgorithm),
+ },
+ },
+ {
+ name: "invalid object lock headers",
+ input: testInput{
+ locals: defaultLocals,
+ headers: map[string]string{
+ "X-Amz-Copy-Source": "bucket/object",
+ "X-Amz-Object-Lock-Mode": "GOVERNANCE",
+ },
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ },
+ },
+ err: s3err.GetAPIError(s3err.ErrObjectLockInvalidHeaders),
+ },
+ },
+ {
+ name: "backend returns error",
+ input: testInput{
+ locals: defaultLocals,
+ beErr: s3err.GetAPIError(s3err.ErrNoSuchBucket),
+ beRes: s3response.CopyObjectOutput{},
+ headers: map[string]string{
+ "X-Amz-Copy-Source": "bucket/object",
+ },
+ },
+ output: testOutput{
+ response: &Response{
+ Data: nilResp,
+ Headers: map[string]*string{
+ "x-amz-copy-source-version-id": nil,
+ "x-amz-version-id": nil,
+ },
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ EventName: s3event.EventObjectCreatedCopy,
+ },
+ },
+ err: s3err.GetAPIError(s3err.ErrNoSuchBucket),
+ },
+ },
+ {
+ name: "successful response",
+ input: testInput{
+ locals: defaultLocals,
+ headers: map[string]string{
+ "X-Amz-Copy-Source": "bucket/object",
+ },
+ beRes: s3response.CopyObjectOutput{
+ CopySourceVersionId: utils.GetStringPtr("copySourceVersionId"),
+ VersionId: utils.GetStringPtr("versionId"),
+ CopyObjectResult: &s3response.CopyObjectResult{
+ ETag: utils.GetStringPtr("ETag"),
+ },
+ },
+ },
+ output: testOutput{
+ response: &Response{
+ Data: &s3response.CopyObjectResult{
+ ETag: utils.GetStringPtr("ETag"),
+ },
+ Headers: map[string]*string{
+ "x-amz-copy-source-version-id": utils.GetStringPtr("copySourceVersionId"),
+ "x-amz-version-id": utils.GetStringPtr("versionId"),
+ },
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ ObjectETag: utils.GetStringPtr("ETag"),
+ EventName: s3event.EventObjectCreatedCopy,
+ VersionId: utils.GetStringPtr("versionId"),
+ },
+ },
+ },
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ be := &BackendMock{
+ CopyObjectFunc: func(contextMoqParam context.Context, copyObjectInput s3response.CopyObjectInput) (s3response.CopyObjectOutput, error) {
+ return tt.input.beRes.(s3response.CopyObjectOutput), tt.input.beErr
+ },
+ GetBucketPolicyFunc: func(contextMoqParam context.Context, bucket string) ([]byte, error) {
+ return nil, s3err.GetAPIError(s3err.ErrAccessDenied)
+ },
+ }
+
+ ctrl := S3ApiController{
+ be: be,
+ }
+
+ testController(
+ t,
+ ctrl.CopyObject,
+ tt.output.response,
+ tt.output.err,
+ ctxInputs{
+ locals: tt.input.locals,
+ headers: tt.input.headers,
+ })
+ })
+ }
+}
+
+func TestS3ApiController_PutObject(t *testing.T) {
+ str := ""
+ emptyStringPtr := &str
+
+ tests := []struct {
+ name string
+ input testInput
+ output testOutput
+ }{
+ {
+ name: "verify access fails",
+ input: testInput{
+ locals: accessDeniedLocals,
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ },
+ },
+ err: s3err.GetAPIError(s3err.ErrAccessDenied),
+ },
+ },
+ {
+ name: "locked object",
+ input: testInput{
+ locals: defaultLocals,
+ extraMockErr: s3err.GetAPIError(s3err.ErrInvalidRequest),
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ },
+ },
+ err: s3err.GetAPIError(s3err.ErrInvalidRequest),
+ },
+ },
+ {
+ name: "invalid content length",
+ input: testInput{
+ locals: defaultLocals,
+ extraMockErr: s3err.GetAPIError(s3err.ErrObjectLockConfigurationNotFound),
+ headers: map[string]string{
+ "X-Amz-Decoded-Content-Length": "invalid_length",
+ },
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ },
+ },
+ err: s3err.GetAPIError(s3err.ErrInvalidRequest),
+ },
+ },
+ {
+ name: "invalid object lock headers",
+ input: testInput{
+ locals: defaultLocals,
+ extraMockErr: s3err.GetAPIError(s3err.ErrObjectLockConfigurationNotFound),
+ headers: map[string]string{
+ "X-Amz-Object-Lock-Mode": "GOVERNANCE",
+ },
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ },
+ },
+ err: s3err.GetAPIError(s3err.ErrObjectLockInvalidHeaders),
+ },
+ },
+ {
+ name: "invalid checksum headers",
+ input: testInput{
+ locals: defaultLocals,
+ extraMockErr: s3err.GetAPIError(s3err.ErrObjectLockConfigurationNotFound),
+ headers: map[string]string{
+ "X-Amz-Sdk-Checksum-Algorithm": "invalid_algo",
+ },
+ },
+ output: testOutput{
+ response: &Response{
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ },
+ },
+ err: s3err.GetAPIError(s3err.ErrInvalidChecksumAlgorithm),
+ },
+ },
+ {
+ name: "backend returns error",
+ input: testInput{
+ locals: defaultLocals,
+ extraMockErr: s3err.GetAPIError(s3err.ErrObjectLockConfigurationNotFound),
+ beErr: s3err.GetAPIError(s3err.ErrNoSuchBucket),
+ beRes: s3response.PutObjectOutput{},
+ body: []byte("aaa"),
+ headers: map[string]string{
+ "Content-Length": "3",
+ },
+ },
+ output: testOutput{
+ response: &Response{
+ Headers: map[string]*string{
+ "ETag": emptyStringPtr,
+ "x-amz-checksum-crc32": nil,
+ "x-amz-checksum-crc32c": nil,
+ "x-amz-checksum-crc64nvme": nil,
+ "x-amz-checksum-sha1": nil,
+ "x-amz-checksum-sha256": nil,
+ "x-amz-checksum-type": nil,
+ "x-amz-version-id": emptyStringPtr,
+ },
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ EventName: s3event.EventObjectCreatedPut,
+ ContentLength: 3,
+ ObjectSize: 3,
+ ObjectETag: emptyStringPtr,
+ },
+ },
+ err: s3err.GetAPIError(s3err.ErrNoSuchBucket),
+ },
+ },
+ {
+ name: "successful response",
+ input: testInput{
+ locals: map[utils.ContextKey]any{
+ utils.ContextKeyIsRoot: true,
+ utils.ContextKeyParsedAcl: auth.ACL{
+ Owner: "root",
+ },
+ utils.ContextKeyAccount: auth.Account{
+ Access: "root",
+ Role: auth.RoleAdmin,
+ },
+ utils.ContextKeyBodyReader: strings.NewReader("something"),
+ },
+ extraMockErr: s3err.GetAPIError(s3err.ErrObjectLockConfigurationNotFound),
+ headers: map[string]string{
+ "Content-Length": "3",
+ },
+ body: []byte("aaa"),
+ beRes: s3response.PutObjectOutput{
+ ETag: "ETag",
+ ChecksumCRC32: utils.GetStringPtr("crc32"),
+ ChecksumCRC32C: utils.GetStringPtr("crc32c"),
+ ChecksumSHA1: utils.GetStringPtr("sha1"),
+ ChecksumSHA256: utils.GetStringPtr("sha256"),
+ ChecksumCRC64NVME: utils.GetStringPtr("crc64nvme"),
+ ChecksumType: types.ChecksumTypeComposite,
+ VersionID: "versionId",
+ },
+ },
+ output: testOutput{
+ response: &Response{
+ Headers: map[string]*string{
+ "ETag": utils.GetStringPtr("ETag"),
+ "x-amz-checksum-crc32": utils.GetStringPtr("crc32"),
+ "x-amz-checksum-crc32c": utils.GetStringPtr("crc32c"),
+ "x-amz-checksum-crc64nvme": utils.GetStringPtr("crc64nvme"),
+ "x-amz-checksum-sha1": utils.GetStringPtr("sha1"),
+ "x-amz-checksum-sha256": utils.GetStringPtr("sha256"),
+ "x-amz-checksum-type": utils.GetStringPtr(string(types.ChecksumTypeComposite)),
+ "x-amz-version-id": utils.GetStringPtr("versionId"),
+ },
+ MetaOpts: &MetaOptions{
+ BucketOwner: "root",
+ ObjectETag: utils.GetStringPtr("ETag"),
+ EventName: s3event.EventObjectCreatedPut,
+ ContentLength: 3,
+ ObjectSize: 3,
+ },
+ },
+ },
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ be := &BackendMock{
+ PutObjectFunc: func(contextMoqParam context.Context, putObjectInput s3response.PutObjectInput) (s3response.PutObjectOutput, error) {
+ return tt.input.beRes.(s3response.PutObjectOutput), tt.input.beErr
+ },
+ GetBucketPolicyFunc: func(contextMoqParam context.Context, bucket string) ([]byte, error) {
+ return nil, s3err.GetAPIError(s3err.ErrAccessDenied)
+ },
+ GetObjectLockConfigurationFunc: func(contextMoqParam context.Context, bucket string) ([]byte, error) {
+ return nil, tt.input.extraMockErr
+ },
+ }
+
+ ctrl := S3ApiController{
+ be: be,
+ }
+
+ testController(
+ t,
+ ctrl.PutObject,
+ tt.output.response,
+ tt.output.err,
+ ctxInputs{
+ locals: tt.input.locals,
+ headers: tt.input.headers,
+ body: tt.input.body,
+ })
+ })
+ }
+}
diff --git a/s3api/middlewares/acl-parser.go b/s3api/middlewares/acl-parser.go
index ba1fc21b..53f3ec8a 100644
--- a/s3api/middlewares/acl-parser.go
+++ b/s3api/middlewares/acl-parser.go
@@ -15,65 +15,26 @@
package middlewares
import (
- "net/http"
- "regexp"
- "strings"
-
"github.com/aws/aws-sdk-go-v2/service/s3"
"github.com/gofiber/fiber/v2"
"github.com/versity/versitygw/auth"
"github.com/versity/versitygw/backend"
- "github.com/versity/versitygw/s3api/controllers"
"github.com/versity/versitygw/s3api/utils"
- "github.com/versity/versitygw/s3err"
- "github.com/versity/versitygw/s3log"
)
-var (
- singlePath = regexp.MustCompile(`^/[^/]+/?$`)
-)
-
-func AclParser(be backend.Backend, logger s3log.AuditLogger, readonly bool) fiber.Handler {
+// ParseAcl retreives the bucket acl and stores in the context locals
+// if no bucket is found, it returns 'NoSuchBucket'
+func ParseAcl(be backend.Backend) fiber.Handler {
return func(ctx *fiber.Ctx) error {
- path := ctx.Path()
- pathParts := strings.Split(path, "/")
- bucket := pathParts[1]
- if path == "/" && ctx.Method() == http.MethodGet {
- return ctx.Next()
- }
- if ctx.Method() == http.MethodPatch {
- return ctx.Next()
- }
- if singlePath.MatchString(path) &&
- ctx.Method() == http.MethodPut &&
- !ctx.Request().URI().QueryArgs().Has("acl") &&
- !ctx.Request().URI().QueryArgs().Has("tagging") &&
- !ctx.Request().URI().QueryArgs().Has("versioning") &&
- !ctx.Request().URI().QueryArgs().Has("policy") &&
- !ctx.Request().URI().QueryArgs().Has("object-lock") &&
- !ctx.Request().URI().QueryArgs().Has("ownershipControls") &&
- !ctx.Request().URI().QueryArgs().Has("cors") {
- isRoot, acct := utils.ContextKeyIsRoot.Get(ctx).(bool), utils.ContextKeyAccount.Get(ctx).(auth.Account)
- if err := auth.MayCreateBucket(acct, isRoot); err != nil {
- return controllers.SendXMLResponse(ctx, nil, err, &controllers.MetaOpts{Logger: logger, Action: "CreateBucket"})
- }
- if readonly {
- return controllers.SendXMLResponse(ctx, nil, s3err.GetAPIError(s3err.ErrAccessDenied),
- &controllers.MetaOpts{
- Logger: logger,
- Action: "CreateBucket",
- })
- }
- return ctx.Next()
- }
+ bucket := ctx.Params("bucket")
data, err := be.GetBucketAcl(ctx.Context(), &s3.GetBucketAclInput{Bucket: &bucket})
if err != nil {
- return controllers.SendResponse(ctx, err, &controllers.MetaOpts{Logger: logger})
+ return err
}
parsedAcl, err := auth.ParseACL(data)
if err != nil {
- return controllers.SendResponse(ctx, err, &controllers.MetaOpts{Logger: logger})
+ return err
}
// if owner is not set, set default owner to root account
@@ -82,6 +43,6 @@ func AclParser(be backend.Backend, logger s3log.AuditLogger, readonly bool) fibe
}
utils.ContextKeyParsedAcl.Set(ctx, parsedAcl)
- return ctx.Next()
+ return nil
}
}
diff --git a/s3api/middlewares/admin.go b/s3api/middlewares/admin.go
index e5646cd0..b8626dff 100644
--- a/s3api/middlewares/admin.go
+++ b/s3api/middlewares/admin.go
@@ -15,46 +15,20 @@
package middlewares
import (
- "strings"
-
"github.com/gofiber/fiber/v2"
"github.com/versity/versitygw/auth"
- "github.com/versity/versitygw/metrics"
- "github.com/versity/versitygw/s3api/controllers"
"github.com/versity/versitygw/s3api/utils"
"github.com/versity/versitygw/s3err"
- "github.com/versity/versitygw/s3log"
)
-func IsAdmin(logger s3log.AuditLogger) fiber.Handler {
+// IsAdmin is a middleware that restricts access to admin APIs, allowing only admin users
+func IsAdmin(action string) fiber.Handler {
return func(ctx *fiber.Ctx) error {
acct := utils.ContextKeyAccount.Get(ctx).(auth.Account)
if acct.Role != auth.RoleAdmin {
- path := ctx.Path()
- return controllers.SendResponse(ctx, s3err.GetAPIError(s3err.ErrAdminAccessDenied),
- &controllers.MetaOpts{
- Logger: logger,
- Action: detectAction(path),
- })
+ return s3err.GetAPIError(s3err.ErrAdminAccessDenied)
}
- return ctx.Next()
+ return nil
}
}
-
-func detectAction(path string) (action string) {
- if strings.Contains(path, "create-user") {
- action = metrics.ActionAdminCreateUser
- } else if strings.Contains(path, "update-user") {
- action = metrics.ActionAdminUpdateUser
- } else if strings.Contains(path, "delete-user") {
- action = metrics.ActionAdminDeleteUser
- } else if strings.Contains(path, "list-user") {
- action = metrics.ActionAdminListUsers
- } else if strings.Contains(path, "list-buckets") {
- action = metrics.ActionAdminListBuckets
- } else if strings.Contains(path, "change-bucket-owner") {
- action = metrics.ActionAdminChangeBucketOwner
- }
- return action
-}
diff --git a/s3api/middlewares/authentication.go b/s3api/middlewares/authentication.go
index 54aa70a0..b9afa33f 100644
--- a/s3api/middlewares/authentication.go
+++ b/s3api/middlewares/authentication.go
@@ -25,11 +25,8 @@ import (
"github.com/gofiber/fiber/v2"
"github.com/versity/versitygw/auth"
- "github.com/versity/versitygw/metrics"
- "github.com/versity/versitygw/s3api/controllers"
"github.com/versity/versitygw/s3api/utils"
"github.com/versity/versitygw/s3err"
- "github.com/versity/versitygw/s3log"
)
const (
@@ -42,45 +39,45 @@ type RootUserConfig struct {
Secret string
}
-func VerifyV4Signature(root RootUserConfig, iam auth.IAMService, logger s3log.AuditLogger, mm *metrics.Manager, region string, debug bool) fiber.Handler {
+func VerifyV4Signature(root RootUserConfig, iam auth.IAMService, region string, debug bool) fiber.Handler {
acct := accounts{root: root, iam: iam}
return func(ctx *fiber.Ctx) error {
// The bucket is public, no need to check this signature
if utils.ContextKeyPublicBucket.IsSet(ctx) {
- return ctx.Next()
+ return nil
}
// If ContextKeyAuthenticated is set in context locals, it means it was presigned url case
if utils.ContextKeyAuthenticated.IsSet(ctx) {
- return ctx.Next()
+ return nil
}
authorization := ctx.Get("Authorization")
if authorization == "" {
- return sendResponse(ctx, s3err.GetAPIError(s3err.ErrAuthHeaderEmpty), logger, mm)
+ return s3err.GetAPIError(s3err.ErrAuthHeaderEmpty)
}
authData, err := utils.ParseAuthorization(authorization)
if err != nil {
- return sendResponse(ctx, err, logger, mm)
+ return err
}
if authData.Region != region {
- return sendResponse(ctx, s3err.APIError{
+ return s3err.APIError{
Code: "SignatureDoesNotMatch",
Description: fmt.Sprintf("Credential should be scoped to a valid Region, not %v", authData.Region),
HTTPStatusCode: http.StatusForbidden,
- }, logger, mm)
+ }
}
utils.ContextKeyIsRoot.Set(ctx, authData.Access == root.Access)
account, err := acct.getAccount(authData.Access)
if err == auth.ErrNoSuchUser {
- return sendResponse(ctx, s3err.GetAPIError(s3err.ErrInvalidAccessKeyID), logger, mm)
+ return s3err.GetAPIError(s3err.ErrInvalidAccessKeyID)
}
if err != nil {
- return sendResponse(ctx, err, logger, mm)
+ return err
}
utils.ContextKeyAccount.Set(ctx, account)
@@ -88,23 +85,23 @@ func VerifyV4Signature(root RootUserConfig, iam auth.IAMService, logger s3log.Au
// Check X-Amz-Date header
date := ctx.Get("X-Amz-Date")
if date == "" {
- return sendResponse(ctx, s3err.GetAPIError(s3err.ErrMissingDateHeader), logger, mm)
+ return s3err.GetAPIError(s3err.ErrMissingDateHeader)
}
// Parse the date and check the date validity
tdate, err := time.Parse(iso8601Format, date)
if err != nil {
- return sendResponse(ctx, s3err.GetAPIError(s3err.ErrMalformedDate), logger, mm)
+ return s3err.GetAPIError(s3err.ErrMalformedDate)
}
if date[:8] != authData.Date {
- return sendResponse(ctx, s3err.GetAPIError(s3err.ErrSignatureDateDoesNotMatch), logger, mm)
+ return s3err.GetAPIError(s3err.ErrSignatureDateDoesNotMatch)
}
// Validate the dates difference
err = utils.ValidateDate(tdate)
if err != nil {
- return sendResponse(ctx, err, logger, mm)
+ return err
}
var contentLength int64
@@ -113,7 +110,7 @@ func VerifyV4Signature(root RootUserConfig, iam auth.IAMService, logger s3log.Au
contentLength, err = strconv.ParseInt(contentLengthStr, 10, 64)
//TODO: not sure if InvalidRequest should be returned in this case
if err != nil {
- return sendResponse(ctx, s3err.GetAPIError(s3err.ErrInvalidRequest), logger, mm)
+ return s3err.GetAPIError(s3err.ErrInvalidRequest)
}
}
@@ -136,23 +133,23 @@ func VerifyV4Signature(root RootUserConfig, iam auth.IAMService, logger s3log.Au
return cr
})
if err != nil {
- return sendResponse(ctx, err, logger, mm)
+ return err
}
- return ctx.Next()
+ return nil
}
// Content-Length has to be set for data uploads: PutObject, UploadPart
if contentLengthStr == "" {
- return sendResponse(ctx, s3err.GetAPIError(s3err.ErrMissingContentLength), logger, mm)
+ return s3err.GetAPIError(s3err.ErrMissingContentLength)
}
// the upload limit for big data actions: PutObject, UploadPart
// is 5gb. If the size exceeds the limit, return 'EntityTooLarge' err
if contentLength > maxObjSizeLimit {
- return sendResponse(ctx, s3err.GetAPIError(s3err.ErrEntityTooLarge), logger, mm)
+ return s3err.GetAPIError(s3err.ErrEntityTooLarge)
}
- return ctx.Next()
+ return nil
}
if !utils.IsSpecialPayload(hashPayload) {
@@ -162,16 +159,16 @@ func VerifyV4Signature(root RootUserConfig, iam auth.IAMService, logger s3log.Au
// Compare the calculated hash with the hash provided
if hashPayload != hexPayload {
- return sendResponse(ctx, s3err.GetAPIError(s3err.ErrContentSHA256Mismatch), logger, mm)
+ return s3err.GetAPIError(s3err.ErrContentSHA256Mismatch)
}
}
err = utils.CheckValidSignature(ctx, authData, account.Secret, hashPayload, tdate, contentLength, debug)
if err != nil {
- return sendResponse(ctx, err, logger, mm)
+ return err
}
- return ctx.Next()
+ return nil
}
}
@@ -185,13 +182,9 @@ func (a accounts) getAccount(access string) (auth.Account, error) {
return auth.Account{
Access: a.root.Access,
Secret: a.root.Secret,
- Role: "admin",
+ Role: auth.RoleAdmin,
}, nil
}
return a.iam.GetUserAccount(access)
}
-
-func sendResponse(ctx *fiber.Ctx, err error, logger s3log.AuditLogger, mm *metrics.Manager) error {
- return controllers.SendResponse(ctx, err, &controllers.MetaOpts{Logger: logger, MetricsMng: mm})
-}
diff --git a/s3api/middlewares/bucket-object-name-validator.go b/s3api/middlewares/bucket-object-name-validator.go
index 1abd8a05..f099afd8 100644
--- a/s3api/middlewares/bucket-object-name-validator.go
+++ b/s3api/middlewares/bucket-object-name-validator.go
@@ -15,44 +15,28 @@
package middlewares
import (
- "net/http"
-
"github.com/gofiber/fiber/v2"
- "github.com/versity/versitygw/metrics"
"github.com/versity/versitygw/s3api/utils"
"github.com/versity/versitygw/s3err"
- "github.com/versity/versitygw/s3log"
)
// BucketObjectNameValidator extracts and validates
// the bucket and object names from the request URI.
-func BucketObjectNameValidator(l s3log.AuditLogger, mm *metrics.Manager) fiber.Handler {
+func BucketObjectNameValidator() fiber.Handler {
return func(ctx *fiber.Ctx) error {
- // skip the check for admin apis
- if ctx.Method() == http.MethodPatch {
- return ctx.Next()
- }
-
- path := ctx.Path()
- // skip the check if the operation isn't bucket/object scoped
- // e.g ListBuckets
- if path == "/" {
- return ctx.Next()
- }
-
- bucket, object := parsePath(path)
+ bucket, object := parsePath(ctx.Path())
// check if the provided bucket name is valid
if !utils.IsValidBucketName(bucket) {
- return sendResponse(ctx, s3err.GetAPIError(s3err.ErrInvalidBucketName), l, mm)
+ return s3err.GetAPIError(s3err.ErrInvalidBucketName)
}
// check if the provided object name is valid
// skip for empty objects: e.g bucket operations: HeadBucket...
if object != "" && !utils.IsObjectNameValid(object) {
- return sendResponse(ctx, s3err.GetAPIError(s3err.ErrBadRequest), l, mm)
+ return s3err.GetAPIError(s3err.ErrBadRequest)
}
- return ctx.Next()
+ return nil
}
}
diff --git a/s3api/middlewares/md5.go b/s3api/middlewares/md5.go
index e1b81c65..5cd70b74 100644
--- a/s3api/middlewares/md5.go
+++ b/s3api/middlewares/md5.go
@@ -19,17 +19,15 @@ import (
"io"
"github.com/gofiber/fiber/v2"
- "github.com/versity/versitygw/s3api/controllers"
"github.com/versity/versitygw/s3api/utils"
"github.com/versity/versitygw/s3err"
- "github.com/versity/versitygw/s3log"
)
-func VerifyMD5Body(logger s3log.AuditLogger) fiber.Handler {
+func VerifyMD5Body() fiber.Handler {
return func(ctx *fiber.Ctx) error {
incomingSum := ctx.Get("Content-Md5")
if incomingSum == "" {
- return ctx.Next()
+ return nil
}
if utils.IsBigDataAction(ctx) {
@@ -39,18 +37,18 @@ func VerifyMD5Body(logger s3log.AuditLogger) fiber.Handler {
return r
})
if err != nil {
- return controllers.SendResponse(ctx, err, &controllers.MetaOpts{Logger: logger})
+ return err
}
- return ctx.Next()
+ return nil
}
sum := md5.Sum(ctx.Body())
calculatedSum := utils.Base64SumString(sum[:])
if incomingSum != calculatedSum {
- return controllers.SendResponse(ctx, s3err.GetAPIError(s3err.ErrInvalidDigest), &controllers.MetaOpts{Logger: logger})
+ return s3err.GetAPIError(s3err.ErrInvalidDigest)
}
- return ctx.Next()
+ return nil
}
}
diff --git a/s3api/middlewares/presign-auth.go b/s3api/middlewares/presign-auth.go
index f9d285cd..e872c5d8 100644
--- a/s3api/middlewares/presign-auth.go
+++ b/s3api/middlewares/presign-auth.go
@@ -20,22 +20,20 @@ import (
"github.com/gofiber/fiber/v2"
"github.com/versity/versitygw/auth"
- "github.com/versity/versitygw/metrics"
"github.com/versity/versitygw/s3api/utils"
"github.com/versity/versitygw/s3err"
- "github.com/versity/versitygw/s3log"
)
-func VerifyPresignedV4Signature(root RootUserConfig, iam auth.IAMService, logger s3log.AuditLogger, mm *metrics.Manager, region string, debug bool) fiber.Handler {
+func VerifyPresignedV4Signature(root RootUserConfig, iam auth.IAMService, region string, debug bool) fiber.Handler {
acct := accounts{root: root, iam: iam}
return func(ctx *fiber.Ctx) error {
// The bucket is public, no need to check this signature
if utils.ContextKeyPublicBucket.IsSet(ctx) {
- return ctx.Next()
+ return nil
}
if ctx.Query("X-Amz-Signature") == "" {
- return ctx.Next()
+ return nil
}
// Set in the context the "authenticated" key, in case the authentication succeeds,
@@ -44,17 +42,17 @@ func VerifyPresignedV4Signature(root RootUserConfig, iam auth.IAMService, logger
authData, err := utils.ParsePresignedURIParts(ctx)
if err != nil {
- return sendResponse(ctx, err, logger, mm)
+ return err
}
utils.ContextKeyIsRoot.Set(ctx, authData.Access == root.Access)
account, err := acct.getAccount(authData.Access)
if err == auth.ErrNoSuchUser {
- return sendResponse(ctx, s3err.GetAPIError(s3err.ErrInvalidAccessKeyID), logger, mm)
+ return s3err.GetAPIError(s3err.ErrInvalidAccessKeyID)
}
if err != nil {
- return sendResponse(ctx, err, logger, mm)
+ return err
}
utils.ContextKeyAccount.Set(ctx, account)
@@ -64,32 +62,32 @@ func VerifyPresignedV4Signature(root RootUserConfig, iam auth.IAMService, logger
contentLength, err = strconv.ParseInt(contentLengthStr, 10, 64)
//TODO: not sure if InvalidRequest should be returned in this case
if err != nil {
- return sendResponse(ctx, s3err.GetAPIError(s3err.ErrInvalidRequest), logger, mm)
+ return err
}
}
if utils.IsBigDataAction(ctx) {
// Content-Length has to be set for data uploads: PutObject, UploadPart
if contentLengthStr == "" {
- return sendResponse(ctx, s3err.GetAPIError(s3err.ErrMissingContentLength), logger, mm)
+ return s3err.GetAPIError(s3err.ErrMissingContentLength)
}
// the upload limit for big data actions: PutObject, UploadPart
// is 5gb. If the size exceeds the limit, return 'EntityTooLarge' err
if contentLength > maxObjSizeLimit {
- return sendResponse(ctx, s3err.GetAPIError(s3err.ErrEntityTooLarge), logger, mm)
+ return s3err.GetAPIError(s3err.ErrEntityTooLarge)
}
wrapBodyReader(ctx, func(r io.Reader) io.Reader {
return utils.NewPresignedAuthReader(ctx, r, authData, account.Secret, debug)
})
- return ctx.Next()
+ return nil
}
err = utils.CheckPresignedSignature(ctx, authData, account.Secret, debug)
if err != nil {
- return sendResponse(ctx, err, logger, mm)
+ return err
}
- return ctx.Next()
+ return nil
}
}
diff --git a/s3api/middlewares/public-bucket.go b/s3api/middlewares/public-bucket.go
index 567324ff..52383083 100644
--- a/s3api/middlewares/public-bucket.go
+++ b/s3api/middlewares/public-bucket.go
@@ -24,26 +24,40 @@ import (
"github.com/versity/versitygw/metrics"
"github.com/versity/versitygw/s3api/utils"
"github.com/versity/versitygw/s3err"
- "github.com/versity/versitygw/s3log"
)
-func AuthorizePublicBucketAccess(be backend.Backend, l s3log.AuditLogger, mm *metrics.Manager) fiber.Handler {
+// AuthorizePublicBucketAccess checks if the bucket grants public
+// 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
if ctx.Query("X-Amz-Algorithm") != "" || ctx.Get("Authorization") != "" {
- return ctx.Next()
+ return nil
+ }
+
+ switch s3action {
+ case metrics.ActionListAllMyBuckets:
+ return s3err.GetAPIError(s3err.ErrAccessDenied)
+ case metrics.ActionGetBucketOwnershipControls:
+ return s3err.GetAPIError(s3err.ErrAnonymousGetBucketOwnership)
+ case metrics.ActionPutBucketOwnershipControls, metrics.ActionDeleteBucketOwnershipControls:
+ return s3err.GetAPIError(s3err.ErrAnonymousPutBucketOwnership)
+ case metrics.ActionPutBucketAcl, metrics.ActionPutObjectAcl, metrics.ActionSelectObjectContent, metrics.ActionCreateBucket:
+ return s3err.GetAPIError(s3err.ErrAnonymousRequest)
+ case metrics.ActionCopyObject:
+ return s3err.GetAPIError(s3err.ErrAnonymousCopyObject)
+ case metrics.ActionCreateMultipartUpload:
+ return s3err.GetAPIError(s3err.ErrAnonymousCreateMp)
+ case metrics.ActionUploadPartCopy, metrics.ActionDeleteObjects:
+ // TODO: should be fixed with https://github.com/versity/versitygw/issues/1327
+ // TODO: should be fixed with https://github.com/versity/versitygw/issues/1338
+ return s3err.GetAPIError(s3err.ErrAccessDenied)
}
bucket, object := parsePath(ctx.Path())
-
- action, permission, err := detectS3Action(ctx, object == "")
+ err := auth.VerifyPublicAccess(ctx.Context(), be, policyPermission, permission, bucket, object)
if err != nil {
- return sendResponse(ctx, err, l, mm)
- }
-
- err = auth.VerifyPublicAccess(ctx.Context(), be, action, permission, bucket, object)
- if err != nil {
- return sendResponse(ctx, err, l, mm)
+ return err
}
if utils.IsBigDataAction(ctx) {
@@ -51,7 +65,7 @@ func AuthorizePublicBucketAccess(be backend.Backend, l s3log.AuditLogger, mm *me
if utils.IsUnsignedStreamingPayload(payloadType) {
checksumType, err := utils.ExtractChecksumType(ctx)
if err != nil {
- return sendResponse(ctx, err, l, mm)
+ return err
}
wrapBodyReader(ctx, func(r io.Reader) io.Reader {
@@ -60,232 +74,17 @@ func AuthorizePublicBucketAccess(be backend.Backend, l s3log.AuditLogger, mm *me
return cr
})
if err != nil {
- return sendResponse(ctx, err, l, mm)
+ return err
}
+ } else {
+ utils.ContextKeyBodyReader.Set(ctx, ctx.Request().BodyStream())
}
+
}
utils.ContextKeyPublicBucket.Set(ctx, true)
- return ctx.Next()
- }
-}
-
-func detectS3Action(ctx *fiber.Ctx, isBucketAction bool) (auth.Action, auth.Permission, error) {
- path := ctx.Path()
- // ListBuckets is not publically available
- if path == "/" {
- //TODO: Still not clear what kind of error should be returned in this case(ListBuckets)
- return "", auth.PermissionRead, s3err.GetAPIError(s3err.ErrAccessDenied)
- }
-
- queryArgs := ctx.Context().QueryArgs()
-
- switch ctx.Method() {
- case fiber.MethodPatch:
- // Admin apis should always be protected
- return "", "", s3err.GetAPIError(s3err.ErrAccessDenied)
- case fiber.MethodHead:
- // HeadBucket
- if isBucketAction {
- return auth.ListBucketAction, auth.PermissionRead, nil
- }
-
- // HeadObject
- return auth.GetObjectAction, auth.PermissionRead, nil
- case fiber.MethodGet:
- if isBucketAction {
- if queryArgs.Has("tagging") {
- // GetBucketTagging
- return auth.GetBucketTaggingAction, auth.PermissionRead, nil
- } else if queryArgs.Has("ownershipControls") {
- // GetBucketOwnershipControls
- return auth.GetBucketOwnershipControlsAction, auth.PermissionRead, s3err.GetAPIError(s3err.ErrAnonymousGetBucketOwnership)
- } else if queryArgs.Has("versioning") {
- // GetBucketVersioning
- return auth.GetBucketVersioningAction, auth.PermissionRead, nil
- } else if queryArgs.Has("policy") {
- // GetBucketPolicy
- return auth.GetBucketPolicyAction, auth.PermissionRead, nil
- } else if queryArgs.Has("cors") {
- // GetBucketCors
- return auth.GetBucketCorsAction, auth.PermissionRead, nil
- } else if queryArgs.Has("versions") {
- // ListObjectVersions
- return auth.ListBucketVersionsAction, auth.PermissionRead, nil
- } else if queryArgs.Has("object-lock") {
- // GetObjectLockConfiguration
- return auth.GetBucketObjectLockConfigurationAction, auth.PermissionReadAcp, nil
- } else if queryArgs.Has("acl") {
- // GetBucketAcl
- return auth.GetBucketAclAction, auth.PermissionRead, nil
- } else if queryArgs.Has("uploads") {
- // ListMultipartUploads
- return auth.ListBucketMultipartUploadsAction, auth.PermissionRead, nil
- } else if queryArgs.GetUintOrZero("list-type") == 2 {
- // ListObjectsV2
- return auth.ListBucketAction, auth.PermissionRead, nil
- }
- // All the other requests are considerd as ListObjects in the router
- // no matter what kind of query arguments are provided apart from the ones above
-
- return auth.ListBucketAction, auth.PermissionRead, nil
- }
-
- if queryArgs.Has("tagging") {
- // GetObjectTagging
- return auth.GetObjectTaggingAction, auth.PermissionRead, nil
- } else if queryArgs.Has("retention") {
- // GetObjectRetention
- return auth.GetObjectRetentionAction, auth.PermissionRead, nil
- } else if queryArgs.Has("legal-hold") {
- // GetObjectLegalHold
- return auth.GetObjectLegalHoldAction, auth.PermissionReadAcp, nil
- } else if queryArgs.Has("acl") {
- // GetObjectAcl
- return auth.GetObjectAclAction, auth.PermissionRead, nil
- } else if queryArgs.Has("attributes") {
- // GetObjectAttributes
- return auth.GetObjectAttributesAction, auth.PermissionRead, nil
- } else if queryArgs.Has("uploadId") {
- // ListParts
- return auth.ListMultipartUploadPartsAction, auth.PermissionRead, nil
- }
-
- // All the other requests are considerd as GetObject in the router
- // no matter what kind of query arguments are provided apart from the ones above
- if queryArgs.Has("versionId") {
- return auth.GetObjectVersionAction, auth.PermissionRead, nil
- }
- return auth.GetObjectAction, auth.PermissionRead, nil
- case fiber.MethodPut:
- if isBucketAction {
- if queryArgs.Has("tagging") {
- // PutBucketTagging
- return auth.PutBucketTaggingAction, auth.PermissionWrite, nil
- }
- if queryArgs.Has("ownershipControls") {
- // PutBucketOwnershipControls
- return auth.PutBucketOwnershipControlsAction, auth.PermissionWrite, s3err.GetAPIError(s3err.ErrAnonymousPutBucketOwnership)
- }
- if queryArgs.Has("versioning") {
- // PutBucketVersioning
- return auth.PutBucketVersioningAction, auth.PermissionWrite, nil
- }
- if queryArgs.Has("object-lock") {
- // PutObjectLockConfiguration
- return auth.PutBucketObjectLockConfigurationAction, auth.PermissionWrite, nil
- }
- if queryArgs.Has("cors") {
- // PutBucketCors
- return auth.PutBucketCorsAction, auth.PermissionWrite, nil
- }
- if queryArgs.Has("policy") {
- // PutBucketPolicy
- return auth.PutBucketPolicyAction, auth.PermissionWrite, nil
- }
- if queryArgs.Has("acl") {
- // PutBucketAcl
- return auth.PutBucketAclAction, auth.PermissionWrite, s3err.GetAPIError(s3err.ErrAnonymousRequest)
- }
-
- // All the other rquestes are considered as 'CreateBucket' in the router
- return "", "", s3err.GetAPIError(s3err.ErrAnonymousRequest)
- }
-
- if queryArgs.Has("tagging") {
- // PutObjectTagging
- return auth.PutObjectTaggingAction, auth.PermissionWrite, nil
- }
- if queryArgs.Has("retention") {
- // PutObjectRetention
- return auth.PutObjectRetentionAction, auth.PermissionWrite, nil
- }
- if queryArgs.Has("legal-hold") {
- // PutObjectLegalHold
- return auth.PutObjectLegalHoldAction, auth.PermissionWrite, nil
- }
- if queryArgs.Has("acl") {
- // PutObjectAcl
- return auth.PutObjectAclAction, auth.PermissionWriteAcp, s3err.GetAPIError(s3err.ErrAnonymousRequest)
- }
- if queryArgs.Has("uploadId") && queryArgs.Has("partNumber") {
- if ctx.Get("X-Amz-Copy-Source") != "" {
- // UploadPartCopy
- //TODO: Add public access check for copy-source
- // Return AccessDenied for now
- return auth.PutObjectAction, auth.PermissionWrite, s3err.GetAPIError(s3err.ErrAccessDenied)
- }
-
- utils.ContextKeyBodyReader.Set(ctx, ctx.Request().BodyStream())
- // UploadPart
- return auth.PutObjectAction, auth.PermissionWrite, nil
- }
- if ctx.Get("X-Amz-Copy-Source") != "" {
- return auth.PutObjectAction, auth.PermissionWrite, s3err.GetAPIError(s3err.ErrAnonymousCopyObject)
- }
-
- utils.ContextKeyBodyReader.Set(ctx, ctx.Request().BodyStream())
- // All the other requests are considered as 'PutObject' in the router
- return auth.PutObjectAction, auth.PermissionWrite, nil
- case fiber.MethodPost:
- if isBucketAction {
- // DeleteObjects
- // FIXME: should be fixed with https://github.com/versity/versitygw/issues/1327
- // Return AccessDenied for now
- return auth.DeleteObjectAction, auth.PermissionWrite, s3err.GetAPIError(s3err.ErrAccessDenied)
- }
-
- if queryArgs.Has("restore") {
- return auth.RestoreObjectAction, auth.PermissionWrite, nil
- }
- if queryArgs.Has("select") && ctx.Query("select-type") == "2" {
- // SelectObjectContent
- return auth.GetObjectAction, auth.PermissionRead, s3err.GetAPIError(s3err.ErrAnonymousRequest)
- }
- if queryArgs.Has("uploadId") {
- // CompleteMultipartUpload
- return auth.PutObjectAction, auth.PermissionWrite, nil
- }
-
- // All the other requests are considered as 'CreateMultipartUpload' in the router
- return "", "", s3err.GetAPIError(s3err.ErrAnonymousCreateMp)
- case fiber.MethodDelete:
- if isBucketAction {
- if queryArgs.Has("tagging") {
- // DeleteBucketTagging
- return auth.PutBucketTaggingAction, auth.PermissionWrite, nil
- }
- if queryArgs.Has("ownershipControls") {
- // DeleteBucketOwnershipControls
- return auth.PutBucketOwnershipControlsAction, auth.PermissionWrite, s3err.GetAPIError(s3err.ErrAnonymousPutBucketOwnership)
- }
- if queryArgs.Has("policy") {
- // DeleteBucketPolicy
- return auth.PutBucketPolicyAction, auth.PermissionWrite, nil
- }
- if queryArgs.Has("cors") {
- // DeleteBucketCors
- return auth.PutBucketCorsAction, auth.PermissionWrite, nil
- }
-
- // All the other requests are considered as 'DeleteBucket' in the router
- return auth.DeleteBucketAction, auth.PermissionWrite, nil
- }
-
- if queryArgs.Has("tagging") {
- // DeleteObjectTagging
- return auth.PutObjectTaggingAction, auth.PermissionWrite, nil
- }
- if queryArgs.Has("uploadId") {
- // AbortMultipartUpload
- return auth.AbortMultipartUploadAction, auth.PermissionWrite, nil
- }
- // All the other requests are considered as 'DeleteObject' in the router
- return auth.DeleteObjectAction, auth.PermissionWrite, nil
- default:
- // In no action is detected, return AccessDenied ?
- return "", "", s3err.GetAPIError(s3err.ErrAccessDenied)
+ return nil
}
}
diff --git a/s3api/middlewares/router-utilities.go b/s3api/middlewares/router-utilities.go
new file mode 100644
index 00000000..47929103
--- /dev/null
+++ b/s3api/middlewares/router-utilities.go
@@ -0,0 +1,67 @@
+// Copyright 2023 Versity Software
+// This file is licensed under the Apache License, Version 2.0
+// (the "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package middlewares
+
+import (
+ "github.com/gofiber/fiber/v2"
+ "github.com/versity/versitygw/s3api/utils"
+)
+
+// Evaluates/Matches the provided requst query params
+func MatchQueryArgs(args ...string) fiber.Handler {
+ return func(ctx *fiber.Ctx) error {
+ if utils.ContextKeySkip.IsSet(ctx) {
+ return ctx.Next()
+ }
+ for _, query := range args {
+ if !ctx.Request().URI().QueryArgs().Has(query) {
+ utils.ContextKeySkip.Set(ctx, true)
+ break
+ }
+ }
+ return ctx.Next()
+ }
+}
+
+// Evaluates/Matches the requst header
+func MatchHeader(key string) fiber.Handler {
+ return func(ctx *fiber.Ctx) error {
+ if utils.ContextKeySkip.IsSet(ctx) {
+ return ctx.Next()
+ }
+
+ val := ctx.Get(key)
+ if val == "" {
+ utils.ContextKeySkip.Set(ctx, true)
+ }
+
+ return ctx.Next()
+ }
+}
+
+// Evaluates/Matches the requst query param and value
+func MatchQueryArgWithValue(key, val string) fiber.Handler {
+ return func(ctx *fiber.Ctx) error {
+ if utils.ContextKeySkip.IsSet(ctx) {
+ return ctx.Next()
+ }
+
+ if ctx.Query(key) != val {
+ utils.ContextKeySkip.Set(ctx, true)
+ }
+
+ return ctx.Next()
+ }
+}
diff --git a/s3api/middlewares/url-decoder.go b/s3api/middlewares/url-decoder.go
index fc275ac1..7c4b209b 100644
--- a/s3api/middlewares/url-decoder.go
+++ b/s3api/middlewares/url-decoder.go
@@ -18,19 +18,15 @@ import (
"net/url"
"github.com/gofiber/fiber/v2"
- "github.com/versity/versitygw/metrics"
- "github.com/versity/versitygw/s3api/controllers"
- "github.com/versity/versitygw/s3err"
- "github.com/versity/versitygw/s3log"
)
-func DecodeURL(logger s3log.AuditLogger, mm *metrics.Manager) fiber.Handler {
- return func(ctx *fiber.Ctx) error {
- unescp, err := url.PathUnescape(string(ctx.Request().URI().PathOriginal()))
- if err != nil {
- return controllers.SendResponse(ctx, s3err.GetAPIError(s3err.ErrInvalidURI), &controllers.MetaOpts{Logger: logger, MetricsMng: mm})
- }
- ctx.Path(unescp)
- return ctx.Next()
+// DecodeURL url path unescapes the request url for the gateway
+// to handle some special characters
+func DecodeURL(ctx *fiber.Ctx) error {
+ unescp, err := url.PathUnescape(string(ctx.Request().URI().PathOriginal()))
+ if err != nil {
+ return err
}
+ ctx.Path(unescp)
+ return nil
}
diff --git a/s3api/router.go b/s3api/router.go
index f640cb67..c0694b3d 100644
--- a/s3api/router.go
+++ b/s3api/router.go
@@ -21,6 +21,7 @@ import (
"github.com/versity/versitygw/metrics"
"github.com/versity/versitygw/s3api/controllers"
"github.com/versity/versitygw/s3api/middlewares"
+ "github.com/versity/versitygw/s3err"
"github.com/versity/versitygw/s3event"
"github.com/versity/versitygw/s3log"
)
@@ -29,80 +30,740 @@ type S3ApiRouter struct {
WithAdmSrv bool
}
-func (sa *S3ApiRouter) Init(app *fiber.App, be backend.Backend, iam auth.IAMService, logger s3log.AuditLogger, aLogger s3log.AuditLogger, evs s3event.S3EventSender, mm *metrics.Manager, debug bool, readonly bool) {
- s3ApiController := controllers.New(be, iam, logger, evs, mm, debug, readonly)
+func (sa *S3ApiRouter) Init(app *fiber.App, be backend.Backend, iam auth.IAMService, logger s3log.AuditLogger, aLogger s3log.AuditLogger, evs s3event.S3EventSender, mm metrics.Manager, debug bool, readonly bool, region string, root middlewares.RootUserConfig) {
+ ctrl := controllers.New(be, iam, logger, evs, mm, debug, readonly)
+ adminServices := &controllers.Services{
+ Logger: aLogger,
+ }
if sa.WithAdmSrv {
adminController := controllers.NewAdminController(iam, be, aLogger)
// CreateUser admin api
- app.Patch("/create-user", middlewares.IsAdmin(logger), adminController.CreateUser)
+ app.Patch("/create-user",
+ controllers.ProcessHandlers(adminController.CreateUser, metrics.ActionAdminCreateUser, adminServices,
+ middlewares.VerifyV4Signature(root, iam, region, debug),
+ middlewares.IsAdmin(metrics.ActionAdminCreateUser),
+ ))
// DeleteUsers admin api
- app.Patch("/delete-user", middlewares.IsAdmin(logger), adminController.DeleteUser)
+ app.Patch("/delete-user",
+ controllers.ProcessHandlers(adminController.DeleteUser, metrics.ActionAdminDeleteUser, adminServices,
+ middlewares.VerifyV4Signature(root, iam, region, debug),
+ middlewares.IsAdmin(metrics.ActionAdminDeleteUser),
+ ))
// UpdateUser admin api
- app.Patch("/update-user", middlewares.IsAdmin(logger), adminController.UpdateUser)
+ app.Patch("/update-user",
+ controllers.ProcessHandlers(adminController.UpdateUser, metrics.ActionAdminUpdateUser, adminServices,
+ middlewares.VerifyV4Signature(root, iam, region, debug),
+ middlewares.IsAdmin(metrics.ActionAdminUpdateUser),
+ ))
// ListUsers admin api
- app.Patch("/list-users", middlewares.IsAdmin(logger), adminController.ListUsers)
+ app.Patch("/list-users",
+ controllers.ProcessHandlers(adminController.ListUsers, metrics.ActionAdminListUsers, adminServices,
+ middlewares.VerifyV4Signature(root, iam, region, debug),
+ middlewares.IsAdmin(metrics.ActionAdminListUsers),
+ ))
// ChangeBucketOwner admin api
- app.Patch("/change-bucket-owner", middlewares.IsAdmin(logger), adminController.ChangeBucketOwner)
+ app.Patch("/change-bucket-owner",
+ controllers.ProcessHandlers(adminController.ChangeBucketOwner, metrics.ActionAdminChangeBucketOwner, adminServices,
+ middlewares.VerifyV4Signature(root, iam, region, debug),
+ middlewares.IsAdmin(metrics.ActionAdminChangeBucketOwner),
+ ))
// ListBucketsAndOwners admin api
- app.Patch("/list-buckets", middlewares.IsAdmin(logger), adminController.ListBuckets)
+ app.Patch("/list-buckets",
+ controllers.ProcessHandlers(adminController.ListBuckets, metrics.ActionAdminListBuckets, adminServices,
+ middlewares.VerifyV4Signature(root, iam, region, debug),
+ middlewares.IsAdmin(metrics.ActionAdminListBuckets),
+ ))
+ }
+
+ services := &controllers.Services{
+ Logger: logger,
+ EventSender: evs,
+ MetricsManager: mm,
}
// ListBuckets action
- app.Get("/", s3ApiController.ListBuckets)
+ app.Get("/",
+ controllers.ProcessHandlers(
+ ctrl.ListBuckets,
+ metrics.ActionListAllMyBuckets,
+ services,
+ middlewares.AuthorizePublicBucketAccess(be, metrics.ActionListAllMyBuckets, "", auth.PermissionRead),
+ middlewares.VerifyPresignedV4Signature(root, iam, region, debug),
+ middlewares.VerifyV4Signature(root, iam, region, debug),
+ middlewares.VerifyMD5Body(),
+ ))
- // CreateBucket action
- // PutBucketAcl action
- app.Put("/:bucket", s3ApiController.PutBucketActions)
+ bucketRouter := app.Group("/:bucket")
+ objectRouter := app.Group("/:bucket/*")
- // DeleteBucket action
- app.Delete("/:bucket", s3ApiController.DeleteBucket)
+ // PUT bucket operations
+ bucketRouter.Put("",
+ middlewares.MatchQueryArgs("tagging"),
+ controllers.ProcessHandlers(
+ ctrl.PutBucketTagging,
+ metrics.ActionPutBucketTagging,
+ services,
+ middlewares.BucketObjectNameValidator(),
+ middlewares.AuthorizePublicBucketAccess(be, metrics.ActionPutBucketTagging, auth.PutBucketTaggingAction, auth.PermissionWrite),
+ middlewares.VerifyPresignedV4Signature(root, iam, region, debug),
+ middlewares.VerifyV4Signature(root, iam, region, debug),
+ middlewares.VerifyMD5Body(),
+ middlewares.ParseAcl(be),
+ ))
+ bucketRouter.Put("",
+ middlewares.MatchQueryArgs("ownershipControls"),
+ controllers.ProcessHandlers(
+ ctrl.PutBucketOwnershipControls,
+ metrics.ActionPutBucketOwnershipControls,
+ services,
+ middlewares.BucketObjectNameValidator(),
+ middlewares.AuthorizePublicBucketAccess(be, metrics.ActionPutBucketOwnershipControls, auth.PutBucketOwnershipControlsAction, auth.PermissionWrite),
+ middlewares.VerifyPresignedV4Signature(root, iam, region, debug),
+ middlewares.VerifyV4Signature(root, iam, region, debug),
+ middlewares.VerifyMD5Body(),
+ middlewares.ParseAcl(be),
+ ))
+ bucketRouter.Put("",
+ middlewares.MatchQueryArgs("versioning"),
+ controllers.ProcessHandlers(
+ ctrl.PutBucketVersioning,
+ metrics.ActionPutBucketVersioning,
+ services,
+ middlewares.BucketObjectNameValidator(),
+ middlewares.AuthorizePublicBucketAccess(be, metrics.ActionPutBucketVersioning, auth.PutBucketVersioningAction, auth.PermissionWrite),
+ middlewares.VerifyPresignedV4Signature(root, iam, region, debug),
+ middlewares.VerifyV4Signature(root, iam, region, debug),
+ middlewares.VerifyMD5Body(),
+ middlewares.ParseAcl(be),
+ ))
+ bucketRouter.Put("",
+ middlewares.MatchQueryArgs("object-lock"),
+ controllers.ProcessHandlers(
+ ctrl.PutObjectLockConfiguration,
+ metrics.ActionPutObjectLockConfiguration,
+ services,
+ middlewares.BucketObjectNameValidator(),
+ middlewares.AuthorizePublicBucketAccess(be, metrics.ActionPutObjectLockConfiguration, auth.PutBucketObjectLockConfigurationAction, auth.PermissionWrite),
+ middlewares.VerifyPresignedV4Signature(root, iam, region, debug),
+ middlewares.VerifyV4Signature(root, iam, region, debug),
+ middlewares.VerifyMD5Body(),
+ middlewares.ParseAcl(be),
+ ))
+ bucketRouter.Put("",
+ middlewares.MatchQueryArgs("cors"),
+ controllers.ProcessHandlers(
+ ctrl.PutBucketCors,
+ metrics.ActionPutBucketCors,
+ services,
+ middlewares.BucketObjectNameValidator(),
+ middlewares.AuthorizePublicBucketAccess(be, metrics.ActionPutBucketCors, auth.PutBucketCorsAction, auth.PermissionWrite),
+ middlewares.VerifyPresignedV4Signature(root, iam, region, debug),
+ middlewares.VerifyV4Signature(root, iam, region, debug),
+ middlewares.VerifyMD5Body(),
+ middlewares.ParseAcl(be),
+ ))
+ bucketRouter.Put("",
+ middlewares.MatchQueryArgs("policy"),
+ controllers.ProcessHandlers(
+ ctrl.PutBucketPolicy,
+ metrics.ActionPutBucketPolicy,
+ services,
+ middlewares.BucketObjectNameValidator(),
+ middlewares.AuthorizePublicBucketAccess(be, metrics.ActionPutBucketPolicy, auth.PutBucketPolicyAction, auth.PermissionWrite),
+ middlewares.VerifyPresignedV4Signature(root, iam, region, debug),
+ middlewares.VerifyV4Signature(root, iam, region, debug),
+ middlewares.VerifyMD5Body(),
+ middlewares.ParseAcl(be),
+ ))
+ bucketRouter.Put("",
+ middlewares.MatchQueryArgs("acl"),
+ controllers.ProcessHandlers(
+ ctrl.PutBucketAcl,
+ metrics.ActionPutBucketAcl,
+ services,
+ middlewares.BucketObjectNameValidator(),
+ middlewares.AuthorizePublicBucketAccess(be, metrics.ActionPutBucketAcl, auth.PutBucketAclAction, auth.PermissionWriteAcp),
+ middlewares.VerifyPresignedV4Signature(root, iam, region, debug),
+ middlewares.VerifyV4Signature(root, iam, region, debug),
+ middlewares.VerifyMD5Body(),
+ middlewares.ParseAcl(be),
+ ))
+ bucketRouter.Put("",
+ controllers.ProcessHandlers(
+ ctrl.CreateBucket,
+ metrics.ActionCreateBucket,
+ services,
+ middlewares.BucketObjectNameValidator(),
+ middlewares.AuthorizePublicBucketAccess(be, metrics.ActionCreateBucket, auth.CreateBucketAction, auth.PermissionWrite),
+ middlewares.VerifyPresignedV4Signature(root, iam, region, debug),
+ middlewares.VerifyV4Signature(root, iam, region, debug),
+ middlewares.VerifyMD5Body(),
+ ))
- // HeadBucket
- app.Head("/:bucket", s3ApiController.HeadBucket)
+ // HeadBucket action
+ bucketRouter.Head("",
+ controllers.ProcessHandlers(
+ ctrl.HeadBucket,
+ metrics.ActionHeadBucket,
+ services,
+ middlewares.BucketObjectNameValidator(),
+ middlewares.AuthorizePublicBucketAccess(be, metrics.ActionHeadBucket, auth.ListBucketAction, auth.PermissionRead),
+ middlewares.VerifyPresignedV4Signature(root, iam, region, debug),
+ middlewares.VerifyV4Signature(root, iam, region, debug),
+ middlewares.VerifyMD5Body(),
+ middlewares.ParseAcl(be),
+ ))
- // GetBucketAcl action
- // ListMultipartUploads action
- // ListObjects action
- // ListObjectsV2 action
- app.Get("/:bucket", s3ApiController.ListActions)
+ // DELETE bucket operations
+ bucketRouter.Delete("",
+ middlewares.MatchQueryArgs("tagging"),
+ controllers.ProcessHandlers(
+ ctrl.DeleteBucketTagging,
+ metrics.ActionDeleteBucketTagging,
+ services,
+ middlewares.BucketObjectNameValidator(),
+ middlewares.AuthorizePublicBucketAccess(be, metrics.ActionDeleteBucketTagging, auth.PutBucketTaggingAction, auth.PermissionWrite),
+ middlewares.VerifyPresignedV4Signature(root, iam, region, debug),
+ middlewares.VerifyV4Signature(root, iam, region, debug),
+ middlewares.VerifyMD5Body(),
+ middlewares.ParseAcl(be),
+ ))
+ bucketRouter.Delete("",
+ middlewares.MatchQueryArgs("ownershipControls"),
+ controllers.ProcessHandlers(
+ ctrl.DeleteBucketOwnershipControls,
+ metrics.ActionDeleteBucketOwnershipControls,
+ services,
+ middlewares.BucketObjectNameValidator(),
+ middlewares.AuthorizePublicBucketAccess(be, metrics.ActionDeleteBucketOwnershipControls, auth.PutBucketOwnershipControlsAction, auth.PermissionWrite),
+ middlewares.VerifyPresignedV4Signature(root, iam, region, debug),
+ middlewares.VerifyV4Signature(root, iam, region, debug),
+ middlewares.VerifyMD5Body(),
+ middlewares.ParseAcl(be),
+ ))
+ bucketRouter.Delete("",
+ middlewares.MatchQueryArgs("policy"),
+ controllers.ProcessHandlers(
+ ctrl.DeleteBucketPolicy,
+ metrics.ActionDeleteBucketPolicy,
+ services,
+ middlewares.BucketObjectNameValidator(),
+ middlewares.AuthorizePublicBucketAccess(be, metrics.ActionDeleteBucketPolicy, auth.PutBucketPolicyAction, auth.PermissionWrite),
+ middlewares.VerifyPresignedV4Signature(root, iam, region, debug),
+ middlewares.VerifyV4Signature(root, iam, region, debug),
+ middlewares.VerifyMD5Body(),
+ middlewares.ParseAcl(be),
+ ))
+ bucketRouter.Delete("",
+ middlewares.MatchQueryArgs("cors"),
+ controllers.ProcessHandlers(
+ ctrl.DeleteBucketCors,
+ metrics.ActionDeleteBucketCors,
+ services,
+ middlewares.BucketObjectNameValidator(),
+ middlewares.AuthorizePublicBucketAccess(be, metrics.ActionDeleteBucketCors, auth.PutBucketCorsAction, auth.PermissionWrite),
+ middlewares.VerifyPresignedV4Signature(root, iam, region, debug),
+ middlewares.VerifyV4Signature(root, iam, region, debug),
+ middlewares.VerifyMD5Body(),
+ middlewares.ParseAcl(be),
+ ))
+ bucketRouter.Delete("",
+ controllers.ProcessHandlers(
+ ctrl.DeleteBucket,
+ metrics.ActionDeleteBucket,
+ services,
+ middlewares.BucketObjectNameValidator(),
+ middlewares.AuthorizePublicBucketAccess(be, metrics.ActionDeleteBucket, auth.DeleteBucketAction, auth.PermissionWrite),
+ middlewares.VerifyPresignedV4Signature(root, iam, region, debug),
+ middlewares.VerifyV4Signature(root, iam, region, debug),
+ middlewares.VerifyMD5Body(),
+ middlewares.ParseAcl(be),
+ ))
- // HeadObject action
- app.Head("/:bucket/:key/*", s3ApiController.HeadObject)
-
- // GetObjectAcl action
- // GetObject action
- // ListObjectParts action
- // GetObjectTagging action
- // ListParts action
- // GetObjectAttributes action
- app.Get("/:bucket/:key/*", s3ApiController.GetActions)
-
- // DeleteObject action
- // AbortMultipartUpload action
- // DeleteObjectTagging action
- app.Delete("/:bucket/:key/*", s3ApiController.DeleteActions)
+ // GET bucket operations
+ bucketRouter.Get("",
+ middlewares.MatchQueryArgs("tagging"),
+ controllers.ProcessHandlers(
+ ctrl.GetBucketTagging,
+ metrics.ActionGetBucketTagging,
+ services,
+ middlewares.BucketObjectNameValidator(),
+ middlewares.AuthorizePublicBucketAccess(be, metrics.ActionGetBucketTagging, auth.GetBucketTaggingAction, auth.PermissionRead),
+ middlewares.VerifyPresignedV4Signature(root, iam, region, debug),
+ middlewares.VerifyV4Signature(root, iam, region, debug),
+ middlewares.VerifyMD5Body(),
+ middlewares.ParseAcl(be),
+ ))
+ bucketRouter.Get("",
+ middlewares.MatchQueryArgs("ownershipControls"),
+ controllers.ProcessHandlers(
+ ctrl.GetBucketOwnershipControls,
+ metrics.ActionGetBucketOwnershipControls,
+ services,
+ middlewares.BucketObjectNameValidator(),
+ middlewares.AuthorizePublicBucketAccess(be, metrics.ActionGetBucketOwnershipControls, auth.GetBucketOwnershipControlsAction, auth.PermissionRead),
+ middlewares.VerifyPresignedV4Signature(root, iam, region, debug),
+ middlewares.VerifyV4Signature(root, iam, region, debug),
+ middlewares.VerifyMD5Body(),
+ middlewares.ParseAcl(be),
+ ))
+ bucketRouter.Get("",
+ middlewares.MatchQueryArgs("versioning"),
+ controllers.ProcessHandlers(
+ ctrl.GetBucketVersioning,
+ metrics.ActionGetBucketVersioning,
+ services,
+ middlewares.BucketObjectNameValidator(),
+ middlewares.AuthorizePublicBucketAccess(be, metrics.ActionGetBucketVersioning, auth.GetBucketVersioningAction, auth.PermissionRead),
+ middlewares.VerifyPresignedV4Signature(root, iam, region, debug),
+ middlewares.VerifyV4Signature(root, iam, region, debug),
+ middlewares.VerifyMD5Body(),
+ middlewares.ParseAcl(be),
+ ))
+ bucketRouter.Get("",
+ middlewares.MatchQueryArgs("policy"),
+ controllers.ProcessHandlers(
+ ctrl.GetBucketPolicy,
+ metrics.ActionGetBucketPolicy,
+ services,
+ middlewares.BucketObjectNameValidator(),
+ middlewares.AuthorizePublicBucketAccess(be, metrics.ActionGetBucketPolicy, auth.GetBucketPolicyAction, auth.PermissionRead),
+ middlewares.VerifyPresignedV4Signature(root, iam, region, debug),
+ middlewares.VerifyV4Signature(root, iam, region, debug),
+ middlewares.VerifyMD5Body(),
+ middlewares.ParseAcl(be),
+ ))
+ bucketRouter.Get("",
+ middlewares.MatchQueryArgs("cors"),
+ controllers.ProcessHandlers(
+ ctrl.GetBucketCors,
+ metrics.ActionGetBucketCors,
+ services,
+ middlewares.BucketObjectNameValidator(),
+ middlewares.AuthorizePublicBucketAccess(be, metrics.ActionGetBucketCors, auth.GetBucketCorsAction, auth.PermissionRead),
+ middlewares.VerifyPresignedV4Signature(root, iam, region, debug),
+ middlewares.VerifyV4Signature(root, iam, region, debug),
+ middlewares.VerifyMD5Body(),
+ middlewares.ParseAcl(be),
+ ))
+ bucketRouter.Get("",
+ middlewares.MatchQueryArgs("object-lock"),
+ controllers.ProcessHandlers(
+ ctrl.GetObjectLockConfiguration,
+ metrics.ActionGetObjectLockConfiguration,
+ services,
+ middlewares.BucketObjectNameValidator(),
+ middlewares.AuthorizePublicBucketAccess(be, metrics.ActionGetObjectLockConfiguration, auth.GetBucketObjectLockConfigurationAction, auth.PermissionRead),
+ middlewares.VerifyPresignedV4Signature(root, iam, region, debug),
+ middlewares.VerifyV4Signature(root, iam, region, debug),
+ middlewares.VerifyMD5Body(),
+ middlewares.ParseAcl(be),
+ ))
+ bucketRouter.Get("",
+ middlewares.MatchQueryArgs("acl"),
+ controllers.ProcessHandlers(
+ ctrl.GetBucketAcl,
+ metrics.ActionGetBucketAcl,
+ services,
+ middlewares.BucketObjectNameValidator(),
+ middlewares.AuthorizePublicBucketAccess(be, metrics.ActionGetBucketAcl, auth.GetBucketAclAction, auth.PermissionReadAcp),
+ middlewares.VerifyPresignedV4Signature(root, iam, region, debug),
+ middlewares.VerifyV4Signature(root, iam, region, debug),
+ middlewares.VerifyMD5Body(),
+ middlewares.ParseAcl(be),
+ ))
+ bucketRouter.Get("",
+ middlewares.MatchQueryArgs("uploads"),
+ controllers.ProcessHandlers(
+ ctrl.ListMultipartUploads,
+ metrics.ActionListMultipartUploads,
+ services,
+ middlewares.BucketObjectNameValidator(),
+ middlewares.AuthorizePublicBucketAccess(be, metrics.ActionListMultipartUploads, auth.ListBucketMultipartUploadsAction, auth.PermissionRead),
+ middlewares.VerifyPresignedV4Signature(root, iam, region, debug),
+ middlewares.VerifyV4Signature(root, iam, region, debug),
+ middlewares.VerifyMD5Body(),
+ middlewares.ParseAcl(be),
+ ))
+ bucketRouter.Get("",
+ middlewares.MatchQueryArgs("versions"),
+ controllers.ProcessHandlers(
+ ctrl.ListObjectVersions,
+ metrics.ActionListObjectVersions,
+ services,
+ middlewares.BucketObjectNameValidator(),
+ middlewares.AuthorizePublicBucketAccess(be, metrics.ActionListObjectVersions, auth.ListBucketVersionsAction, auth.PermissionRead),
+ middlewares.VerifyPresignedV4Signature(root, iam, region, debug),
+ middlewares.VerifyV4Signature(root, iam, region, debug),
+ middlewares.VerifyMD5Body(),
+ middlewares.ParseAcl(be),
+ ))
+ bucketRouter.Get("",
+ middlewares.MatchQueryArgWithValue("list-type", "2"),
+ controllers.ProcessHandlers(
+ ctrl.ListObjectsV2,
+ metrics.ActionListObjectsV2,
+ services,
+ middlewares.BucketObjectNameValidator(),
+ middlewares.AuthorizePublicBucketAccess(be, metrics.ActionListObjectsV2, auth.ListBucketAction, auth.PermissionRead),
+ middlewares.VerifyPresignedV4Signature(root, iam, region, debug),
+ middlewares.VerifyV4Signature(root, iam, region, debug),
+ middlewares.VerifyMD5Body(),
+ middlewares.ParseAcl(be),
+ ))
+ bucketRouter.Get("",
+ controllers.ProcessHandlers(
+ ctrl.ListObjects,
+ metrics.ActionListObjects,
+ services,
+ middlewares.BucketObjectNameValidator(),
+ middlewares.AuthorizePublicBucketAccess(be, metrics.ActionListObjects, auth.ListBucketAction, auth.PermissionRead),
+ middlewares.VerifyPresignedV4Signature(root, iam, region, debug),
+ middlewares.VerifyV4Signature(root, iam, region, debug),
+ middlewares.VerifyMD5Body(),
+ middlewares.ParseAcl(be),
+ ))
// DeleteObjects action
- app.Post("/:bucket", s3ApiController.DeleteObjects)
+ bucketRouter.Post("",
+ middlewares.MatchQueryArgs("delete"),
+ controllers.ProcessHandlers(
+ ctrl.DeleteObjects,
+ metrics.ActionDeleteObjects,
+ services,
+ middlewares.BucketObjectNameValidator(),
+ middlewares.AuthorizePublicBucketAccess(be, metrics.ActionDeleteObjects, auth.DeleteObjectAction, auth.PermissionWrite),
+ middlewares.VerifyPresignedV4Signature(root, iam, region, debug),
+ middlewares.VerifyV4Signature(root, iam, region, debug),
+ middlewares.VerifyMD5Body(),
+ middlewares.ParseAcl(be),
+ ))
- // CompleteMultipartUpload action
- // CreateMultipartUpload
- // RestoreObject action
- // SelectObjectContent action
- app.Post("/:bucket/:key/*", s3ApiController.CreateActions)
+ // HeadObject
+ objectRouter.Head("",
+ controllers.ProcessHandlers(
+ ctrl.HeadObject,
+ metrics.ActionHeadObject,
+ services,
+ middlewares.BucketObjectNameValidator(),
+ middlewares.AuthorizePublicBucketAccess(be, metrics.ActionHeadObject, auth.GetObjectAction, auth.PermissionRead),
+ middlewares.VerifyPresignedV4Signature(root, iam, region, debug),
+ middlewares.VerifyV4Signature(root, iam, region, debug),
+ middlewares.VerifyMD5Body(),
+ middlewares.ParseAcl(be),
+ ))
- // CopyObject action
- // PutObject action
- // UploadPart action
- // UploadPartCopy action
- // PutObjectTagging action
- // PutObjectAcl action
- app.Put("/:bucket/:key/*", s3ApiController.PutActions)
+ // GET object operations
+ objectRouter.Get("",
+ middlewares.MatchQueryArgs("tagging"),
+ controllers.ProcessHandlers(
+ ctrl.GetObjectTagging,
+ metrics.ActionGetObjectTagging,
+ services,
+ middlewares.BucketObjectNameValidator(),
+ middlewares.AuthorizePublicBucketAccess(be, metrics.ActionGetObjectTagging, auth.GetObjectTaggingAction, auth.PermissionRead),
+ middlewares.VerifyPresignedV4Signature(root, iam, region, debug),
+ middlewares.VerifyV4Signature(root, iam, region, debug),
+ middlewares.VerifyMD5Body(),
+ middlewares.ParseAcl(be),
+ ))
+ objectRouter.Get("",
+ middlewares.MatchQueryArgs("retention"),
+ controllers.ProcessHandlers(
+ ctrl.GetObjectRetention,
+ metrics.ActionGetObjectRetention,
+ services,
+ middlewares.BucketObjectNameValidator(),
+ middlewares.AuthorizePublicBucketAccess(be, metrics.ActionGetObjectRetention, auth.GetObjectRetentionAction, auth.PermissionRead),
+ middlewares.VerifyPresignedV4Signature(root, iam, region, debug),
+ middlewares.VerifyV4Signature(root, iam, region, debug),
+ middlewares.VerifyMD5Body(),
+ middlewares.ParseAcl(be),
+ ))
+ objectRouter.Get("",
+ middlewares.MatchQueryArgs("legal-hold"),
+ controllers.ProcessHandlers(
+ ctrl.GetObjectLegalHold,
+ metrics.ActionGetObjectLegalHold,
+ services,
+ middlewares.BucketObjectNameValidator(),
+ middlewares.AuthorizePublicBucketAccess(be, metrics.ActionGetObjectLegalHold, auth.GetObjectLegalHoldAction, auth.PermissionRead),
+ middlewares.VerifyPresignedV4Signature(root, iam, region, debug),
+ middlewares.VerifyV4Signature(root, iam, region, debug),
+ middlewares.VerifyMD5Body(),
+ middlewares.ParseAcl(be),
+ ))
+ objectRouter.Get("",
+ middlewares.MatchQueryArgs("acl"),
+ controllers.ProcessHandlers(
+ ctrl.GetObjectAcl,
+ metrics.ActionGetObjectAcl,
+ services,
+ middlewares.BucketObjectNameValidator(),
+ middlewares.AuthorizePublicBucketAccess(be, metrics.ActionGetObjectAcl, auth.GetObjectAclAction, auth.PermissionReadAcp),
+ middlewares.VerifyPresignedV4Signature(root, iam, region, debug),
+ middlewares.VerifyV4Signature(root, iam, region, debug),
+ middlewares.VerifyMD5Body(),
+ middlewares.ParseAcl(be),
+ ))
+ objectRouter.Get("",
+ middlewares.MatchQueryArgs("attributes"),
+ controllers.ProcessHandlers(
+ ctrl.GetObjectAttributes,
+ metrics.ActionGetObjectAttributes,
+ services,
+ middlewares.BucketObjectNameValidator(),
+ middlewares.AuthorizePublicBucketAccess(be, metrics.ActionGetObjectAttributes, auth.GetObjectAttributesAction, auth.PermissionRead),
+ middlewares.VerifyPresignedV4Signature(root, iam, region, debug),
+ middlewares.VerifyV4Signature(root, iam, region, debug),
+ middlewares.VerifyMD5Body(),
+ middlewares.ParseAcl(be),
+ ))
+ objectRouter.Get("",
+ middlewares.MatchQueryArgs("uploadId"),
+ controllers.ProcessHandlers(
+ ctrl.ListParts,
+ metrics.ActionListParts,
+ services,
+ middlewares.BucketObjectNameValidator(),
+ middlewares.AuthorizePublicBucketAccess(be, metrics.ActionListParts, auth.ListMultipartUploadPartsAction, auth.PermissionRead),
+ middlewares.VerifyPresignedV4Signature(root, iam, region, debug),
+ middlewares.VerifyV4Signature(root, iam, region, debug),
+ middlewares.VerifyMD5Body(),
+ middlewares.ParseAcl(be),
+ ))
+ objectRouter.Get("",
+ controllers.ProcessHandlers(
+ ctrl.GetObject,
+ metrics.ActionGetObject,
+ services,
+ middlewares.BucketObjectNameValidator(),
+ middlewares.AuthorizePublicBucketAccess(be, metrics.ActionGetObject, auth.GetObjectAction, auth.PermissionRead),
+ middlewares.VerifyPresignedV4Signature(root, iam, region, debug),
+ middlewares.VerifyV4Signature(root, iam, region, debug),
+ middlewares.VerifyMD5Body(),
+ middlewares.ParseAcl(be),
+ ))
+
+ // DELETE object operations
+ objectRouter.Delete("",
+ middlewares.MatchQueryArgs("tagging"),
+ controllers.ProcessHandlers(
+ ctrl.DeleteObjectTagging,
+ metrics.ActionDeleteObjectTagging,
+ services,
+ middlewares.BucketObjectNameValidator(),
+ middlewares.AuthorizePublicBucketAccess(be, metrics.ActionDeleteObjectTagging, auth.DeleteObjectTaggingAction, auth.PermissionWrite),
+ middlewares.VerifyPresignedV4Signature(root, iam, region, debug),
+ middlewares.VerifyV4Signature(root, iam, region, debug),
+ middlewares.VerifyMD5Body(),
+ middlewares.ParseAcl(be),
+ ))
+ objectRouter.Delete("",
+ middlewares.MatchQueryArgs("uploadId"),
+ controllers.ProcessHandlers(
+ ctrl.AbortMultipartUpload,
+ metrics.ActionAbortMultipartUpload,
+ services,
+ middlewares.BucketObjectNameValidator(),
+ middlewares.AuthorizePublicBucketAccess(be, metrics.ActionAbortMultipartUpload, auth.AbortMultipartUploadAction, auth.PermissionWrite),
+ middlewares.VerifyPresignedV4Signature(root, iam, region, debug),
+ middlewares.VerifyV4Signature(root, iam, region, debug),
+ middlewares.VerifyMD5Body(),
+ middlewares.ParseAcl(be),
+ ))
+ objectRouter.Delete("",
+ controllers.ProcessHandlers(
+ ctrl.DeleteObject,
+ metrics.ActionDeleteObject,
+ services,
+ middlewares.BucketObjectNameValidator(),
+ middlewares.AuthorizePublicBucketAccess(be, metrics.ActionDeleteObject, auth.DeleteObjectAction, auth.PermissionWrite),
+ middlewares.VerifyPresignedV4Signature(root, iam, region, debug),
+ middlewares.VerifyV4Signature(root, iam, region, debug),
+ middlewares.VerifyMD5Body(),
+ middlewares.ParseAcl(be),
+ ))
+
+ objectRouter.Post("",
+ middlewares.MatchQueryArgs("restore"),
+ controllers.ProcessHandlers(
+ ctrl.RestoreObject,
+ metrics.ActionRestoreObject,
+ services,
+ middlewares.BucketObjectNameValidator(),
+ middlewares.AuthorizePublicBucketAccess(be, metrics.ActionRestoreObject, auth.RestoreObjectAction, auth.PermissionWrite),
+ middlewares.VerifyPresignedV4Signature(root, iam, region, debug),
+ middlewares.VerifyV4Signature(root, iam, region, debug),
+ middlewares.VerifyMD5Body(),
+ middlewares.ParseAcl(be),
+ ))
+ objectRouter.Post("",
+ middlewares.MatchQueryArgs("select"),
+ middlewares.MatchQueryArgWithValue("select-type", "2"),
+ controllers.ProcessHandlers(
+ ctrl.SelectObjectContent,
+ metrics.ActionSelectObjectContent,
+ services,
+ middlewares.BucketObjectNameValidator(),
+ middlewares.AuthorizePublicBucketAccess(be, metrics.ActionSelectObjectContent, auth.GetObjectAction, auth.PermissionRead),
+ middlewares.VerifyPresignedV4Signature(root, iam, region, debug),
+ middlewares.VerifyV4Signature(root, iam, region, debug),
+ middlewares.VerifyMD5Body(),
+ middlewares.ParseAcl(be),
+ ))
+ objectRouter.Post("",
+ middlewares.MatchQueryArgs("uploadId"),
+ controllers.ProcessHandlers(
+ ctrl.CompleteMultipartUpload,
+ metrics.ActionCompleteMultipartUpload,
+ services,
+ middlewares.BucketObjectNameValidator(),
+ middlewares.AuthorizePublicBucketAccess(be, metrics.ActionCompleteMultipartUpload, auth.PutObjectAction, auth.PermissionWrite),
+ middlewares.VerifyPresignedV4Signature(root, iam, region, debug),
+ middlewares.VerifyV4Signature(root, iam, region, debug),
+ middlewares.VerifyMD5Body(),
+ middlewares.ParseAcl(be),
+ ))
+ objectRouter.Post("",
+ middlewares.MatchQueryArgs("uploads"),
+ controllers.ProcessHandlers(
+ ctrl.CreateMultipartUpload,
+ metrics.ActionCreateMultipartUpload,
+ services,
+ middlewares.BucketObjectNameValidator(),
+ middlewares.AuthorizePublicBucketAccess(be, metrics.ActionCreateMultipartUpload, auth.PutObjectAction, auth.PermissionWrite),
+ middlewares.VerifyPresignedV4Signature(root, iam, region, debug),
+ middlewares.VerifyV4Signature(root, iam, region, debug),
+ middlewares.VerifyMD5Body(),
+ middlewares.ParseAcl(be),
+ ))
+
+ // PUT object operations
+ objectRouter.Put("",
+ middlewares.MatchQueryArgs("tagging"),
+ controllers.ProcessHandlers(
+ ctrl.PutObjectTagging,
+ metrics.ActionPutObjectTagging,
+ services,
+ middlewares.BucketObjectNameValidator(),
+ middlewares.AuthorizePublicBucketAccess(be, metrics.ActionPutObjectTagging, auth.PutObjectTaggingAction, auth.PermissionWrite),
+ middlewares.VerifyPresignedV4Signature(root, iam, region, debug),
+ middlewares.VerifyV4Signature(root, iam, region, debug),
+ middlewares.VerifyMD5Body(),
+ middlewares.ParseAcl(be),
+ ))
+ objectRouter.Put("",
+ middlewares.MatchQueryArgs("retention"),
+ controllers.ProcessHandlers(
+ ctrl.PutObjectRetention,
+ metrics.ActionPutObjectRetention,
+ services,
+ middlewares.BucketObjectNameValidator(),
+ middlewares.AuthorizePublicBucketAccess(be, metrics.ActionPutObjectRetention, auth.PutObjectRetentionAction, auth.PermissionWrite),
+ middlewares.VerifyPresignedV4Signature(root, iam, region, debug),
+ middlewares.VerifyV4Signature(root, iam, region, debug),
+ middlewares.VerifyMD5Body(),
+ middlewares.ParseAcl(be),
+ ))
+ objectRouter.Put("",
+ middlewares.MatchQueryArgs("legal-hold"),
+ controllers.ProcessHandlers(
+ ctrl.PutObjectLegalHold,
+ metrics.ActionPutObjectLegalHold,
+ services,
+ middlewares.BucketObjectNameValidator(),
+ middlewares.AuthorizePublicBucketAccess(be, metrics.ActionPutObjectLegalHold, auth.PutObjectLegalHoldAction, auth.PermissionWrite),
+ middlewares.VerifyPresignedV4Signature(root, iam, region, debug),
+ middlewares.VerifyV4Signature(root, iam, region, debug),
+ middlewares.VerifyMD5Body(),
+ middlewares.ParseAcl(be),
+ ))
+ objectRouter.Put("",
+ middlewares.MatchQueryArgs("acl"),
+ controllers.ProcessHandlers(
+ ctrl.PutObjectAcl,
+ metrics.ActionPutObjectAcl,
+ services,
+ middlewares.BucketObjectNameValidator(),
+ middlewares.AuthorizePublicBucketAccess(be, metrics.ActionPutObjectAcl, auth.PutObjectAclAction, auth.PermissionWriteAcp),
+ middlewares.VerifyPresignedV4Signature(root, iam, region, debug),
+ middlewares.VerifyV4Signature(root, iam, region, debug),
+ middlewares.VerifyMD5Body(),
+ middlewares.ParseAcl(be),
+ ))
+ objectRouter.Put("",
+ middlewares.MatchQueryArgs("uploadId", "partNumber"),
+ middlewares.MatchHeader("X-Amz-Copy-Source"),
+ controllers.ProcessHandlers(
+ ctrl.UploadPartCopy,
+ metrics.ActionUploadPartCopy,
+ services,
+ middlewares.BucketObjectNameValidator(),
+ middlewares.AuthorizePublicBucketAccess(be, metrics.ActionUploadPartCopy, auth.PutObjectAction, auth.PermissionWrite),
+ middlewares.VerifyPresignedV4Signature(root, iam, region, debug),
+ middlewares.VerifyV4Signature(root, iam, region, debug),
+ middlewares.VerifyMD5Body(),
+ middlewares.ParseAcl(be),
+ ))
+ objectRouter.Put("",
+ middlewares.MatchQueryArgs("uploadId", "partNumber"),
+ controllers.ProcessHandlers(
+ ctrl.UploadPart,
+ metrics.ActionUploadPart,
+ services,
+ middlewares.BucketObjectNameValidator(),
+ middlewares.AuthorizePublicBucketAccess(be, metrics.ActionUploadPart, auth.PutObjectAction, auth.PermissionWrite),
+ middlewares.VerifyPresignedV4Signature(root, iam, region, debug),
+ middlewares.VerifyV4Signature(root, iam, region, debug),
+ middlewares.VerifyMD5Body(),
+ middlewares.ParseAcl(be),
+ ))
+
+ // return error if partNumber is used without uploadId
+ objectRouter.Put("",
+ middlewares.MatchQueryArgs("partNumber"),
+ controllers.ProcessHandlers(ctrl.HandleErrorRoute(s3err.GetAPIError(s3err.ErrMissingUploadId)), metrics.ActionUndetected, services))
+
+ // return 'MethodNotAllowed' if uploadId is provided without partNumber
+ // before the router reaches to 'PutObject'
+ objectRouter.Put("",
+ middlewares.MatchQueryArgs("uploadId"),
+ controllers.ProcessHandlers(ctrl.HandleErrorRoute(s3err.GetAPIError(s3err.ErrMethodNotAllowed)), metrics.ActionUndetected, services))
+
+ objectRouter.Put("",
+ middlewares.MatchHeader("X-Amz-Copy-Source"),
+ controllers.ProcessHandlers(
+ ctrl.CopyObject,
+ metrics.ActionCopyObject,
+ services,
+ middlewares.BucketObjectNameValidator(),
+ middlewares.AuthorizePublicBucketAccess(be, metrics.ActionCopyObject, auth.PutObjectAction, auth.PermissionWrite),
+ middlewares.VerifyPresignedV4Signature(root, iam, region, debug),
+ middlewares.VerifyV4Signature(root, iam, region, debug),
+ middlewares.VerifyMD5Body(),
+ middlewares.ParseAcl(be),
+ ))
+ objectRouter.Put("",
+ controllers.ProcessHandlers(
+ ctrl.PutObject,
+ metrics.ActionPutObject,
+ services,
+ middlewares.BucketObjectNameValidator(),
+ middlewares.AuthorizePublicBucketAccess(be, metrics.ActionPutObject, auth.PutObjectAction, auth.PermissionWrite),
+ middlewares.VerifyPresignedV4Signature(root, iam, region, debug),
+ middlewares.VerifyV4Signature(root, iam, region, debug),
+ middlewares.VerifyMD5Body(),
+ middlewares.ParseAcl(be),
+ ))
+
+ // Return MethodNotAllowed for all the unmatched routes
+ app.All("*", controllers.ProcessHandlers(ctrl.HandleErrorRoute(s3err.GetAPIError(s3err.ErrMethodNotAllowed)), metrics.ActionUndetected, services))
}
diff --git a/s3api/router_test.go b/s3api/router_test.go
index 58f01b31..d4ca3351 100644
--- a/s3api/router_test.go
+++ b/s3api/router_test.go
@@ -20,6 +20,7 @@ import (
"github.com/gofiber/fiber/v2"
"github.com/versity/versitygw/auth"
"github.com/versity/versitygw/backend"
+ "github.com/versity/versitygw/s3api/middlewares"
)
func TestS3ApiRouter_Init(t *testing.T) {
@@ -45,7 +46,7 @@ func TestS3ApiRouter_Init(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
- tt.sa.Init(tt.args.app, tt.args.be, tt.args.iam, nil, nil, nil, nil, false, false)
+ tt.sa.Init(tt.args.app, tt.args.be, tt.args.iam, nil, nil, nil, nil, false, false, "us-east-1", middlewares.RootUserConfig{})
})
}
}
diff --git a/s3api/server.go b/s3api/server.go
index 4583e9da..8030b476 100644
--- a/s3api/server.go
+++ b/s3api/server.go
@@ -23,6 +23,7 @@ import (
"github.com/versity/versitygw/auth"
"github.com/versity/versitygw/backend"
"github.com/versity/versitygw/metrics"
+ "github.com/versity/versitygw/s3api/controllers"
"github.com/versity/versitygw/s3api/middlewares"
"github.com/versity/versitygw/s3event"
"github.com/versity/versitygw/s3log"
@@ -50,7 +51,7 @@ func New(
l s3log.AuditLogger,
adminLogger s3log.AuditLogger,
evs s3event.S3EventSender,
- mm *metrics.Manager,
+ mm metrics.Manager,
opts ...Option,
) (*S3ApiServer, error) {
server := &S3ApiServer{
@@ -76,34 +77,25 @@ func New(
return ctx.SendStatus(http.StatusOK)
})
}
- app.Use(middlewares.DecodeURL(l, mm))
+
+ // initilaze the default value setter middleware
+ app.Use(middlewares.SetDefaultValues(root, region))
+
+ // initialize the 'DecodeURL' middleware which
+ // path unescapes the url
+ app.Use(controllers.WrapMiddleware(middlewares.DecodeURL, l, mm))
// initialize host-style parser in virtual domain is specified
if server.virtualDomain != "" {
app.Use(middlewares.HostStyleParser(server.virtualDomain))
}
- // initilaze the default value setter middleware
- app.Use(middlewares.SetDefaultValues(root, region))
-
// initialize the debug logger in debug mode
if server.debug {
app.Use(middlewares.DebugLogger())
}
- // initialize the bucket/object name validator
- app.Use(middlewares.BucketObjectNameValidator(l, mm))
-
- // Public buckets access checker
- app.Use(middlewares.AuthorizePublicBucketAccess(be, l, mm))
-
- // Authentication middlewares
- app.Use(middlewares.VerifyPresignedV4Signature(root, iam, l, mm, region, server.debug))
- app.Use(middlewares.VerifyV4Signature(root, iam, l, mm, region, server.debug))
- app.Use(middlewares.VerifyMD5Body(l))
- app.Use(middlewares.AclParser(be, l, server.readonly))
-
- server.router.Init(app, be, iam, l, adminLogger, evs, mm, server.debug, server.readonly)
+ server.router.Init(app, be, iam, l, adminLogger, evs, mm, server.debug, server.readonly, region, root)
return server, nil
}
diff --git a/s3api/utils/context-keys.go b/s3api/utils/context-keys.go
index 313248aa..2157240b 100644
--- a/s3api/utils/context-keys.go
+++ b/s3api/utils/context-keys.go
@@ -34,6 +34,7 @@ const (
ContextKeyParsedAcl ContextKey = "parsed-acl"
ContextKeySkipResBodyLog ContextKey = "skip-res-body-log"
ContextKeyBodyReader ContextKey = "body-reader"
+ ContextKeySkip ContextKey = "__skip"
)
func (ck ContextKey) Values() []ContextKey {
@@ -60,6 +61,10 @@ func (ck ContextKey) IsSet(ctx *fiber.Ctx) bool {
return val != nil
}
+func (ck ContextKey) Delete(ctx *fiber.Ctx) {
+ ctx.Locals(string(ck), nil)
+}
+
func (ck ContextKey) Get(ctx *fiber.Ctx) any {
return ctx.Locals(string(ck))
}
diff --git a/s3api/utils/utils.go b/s3api/utils/utils.go
index ada265c2..c6f0535c 100644
--- a/s3api/utils/utils.go
+++ b/s3api/utils/utils.go
@@ -664,3 +664,52 @@ func ParseTagging(data []byte, limit TagLimit) (map[string]string, error) {
return tagSet, nil
}
+
+// Returns the provided string pointer
+func GetStringPtr(str string) *string {
+ if str == "" {
+ return nil
+ }
+
+ return &str
+}
+
+// Converts any type to a string pointer
+func ConvertToStringPtr[T any](val T) *string {
+ str := fmt.Sprint(val)
+ if str == "" {
+ return nil
+ }
+ return &str
+}
+
+// Converst any pointer to a string pointer
+func ConvertPtrToStringPtr[T any](val *T) *string {
+ if val == nil {
+ return nil
+ }
+ str := fmt.Sprint(*val)
+ return &str
+}
+
+// Formats the date with the given formatting and returns a string pointer
+func FormatDatePtrToString(date *time.Time, format string) *string {
+ if date == nil {
+ return nil
+ }
+ if date.IsZero() {
+ return nil
+ }
+
+ formatted := date.UTC().Format(format)
+ return &formatted
+}
+
+// GetInt64 returns the value of int64 pointer
+func GetInt64(n *int64) int64 {
+ if n == nil {
+ return 0
+ }
+
+ return *n
+}
diff --git a/s3err/s3err.go b/s3err/s3err.go
index 59aca425..c2c3cc22 100644
--- a/s3err/s3err.go
+++ b/s3err/s3err.go
@@ -168,6 +168,7 @@ const (
ErrInvalidChecksumHeader
ErrTrailerHeaderNotSupported
ErrBadRequest
+ ErrMissingUploadId
// Non-AWS errors
ErrExistingObjectIsDirectory
@@ -732,6 +733,11 @@ var errorCodeResponse = map[ErrorCode]APIError{
Description: "Bad Request",
HTTPStatusCode: http.StatusBadRequest,
},
+ ErrMissingUploadId: {
+ Code: "InvalidArgument",
+ Description: "This operation does not accept partNumber without uploadId",
+ HTTPStatusCode: http.StatusBadRequest,
+ },
// non aws errors
ErrExistingObjectIsDirectory: {
@@ -888,7 +894,7 @@ func GetIncorrectMpObjectSizeErr(expected, actual int64) APIError {
}
}
-func GetInvalidMpObjectSizeErr(val int64) APIError {
+func GetNegatvieMpObjectSizeErr(val int64) APIError {
return APIError{
Code: "InvalidRequest",
Description: fmt.Sprintf("Value for x-amz-mp-object-size header is less than zero: '%v'", val),
@@ -896,6 +902,14 @@ func GetInvalidMpObjectSizeErr(val int64) APIError {
}
}
+func GetInvalidMpObjectSizeErr(val string) APIError {
+ return APIError{
+ Code: "InvalidRequest",
+ Description: fmt.Sprintf("Value for x-amz-mp-object-size header is invalid: '%s'", val),
+ HTTPStatusCode: http.StatusBadRequest,
+ }
+}
+
func CreateExceedingRangeErr(objSize int64) APIError {
return APIError{
Code: "InvalidArgument",
diff --git a/s3response/s3response.go b/s3response/s3response.go
index 810f0b74..223e6f0b 100644
--- a/s3response/s3response.go
+++ b/s3response/s3response.go
@@ -412,6 +412,10 @@ func (r CopyPartResult) MarshalXML(e *xml.Encoder, start xml.StartElement) error
return e.EncodeElement(aux, start)
}
+type CompleteMultipartUploadRequestBody struct {
+ Parts []types.CompletedPart `xml:"Part"`
+}
+
type CompleteMultipartUploadResult struct {
XMLName xml.Name `xml:"http://s3.amazonaws.com/doc/2006-03-01/ CompleteMultipartUploadResult" json:"-"`
Location *string
diff --git a/tests/integration/group-tests.go b/tests/integration/group-tests.go
index 6435e736..e2d7ac7b 100644
--- a/tests/integration/group-tests.go
+++ b/tests/integration/group-tests.go
@@ -637,6 +637,7 @@ func TestFullFlow(s *S3Conf) {
TestGetObjectLegalHold(s)
TestWORMProtection(s)
TestAccessControl(s)
+ TestRouter(s)
// FIXME: The tests should pass for azure as well
// but this issue should be fixed with https://github.com/versity/versitygw/issues/1336
if !s.azureTests {
@@ -652,6 +653,7 @@ func TestPosix(s *S3Conf) {
PutObject_overwrite_file_obj(s)
PutObject_overwrite_file_obj_with_nested_obj(s)
PutObject_dir_obj_with_data(s)
+ PutObject_with_slashes(s)
CreateMultipartUpload_dir_obj(s)
PutObject_name_too_long(s)
HeadObject_name_too_long(s)
@@ -749,6 +751,7 @@ func TestScoutfs(s *S3Conf) {
PutObject_overwrite_file_obj(s)
PutObject_overwrite_file_obj_with_nested_obj(s)
PutObject_dir_obj_with_data(s)
+ PutObject_with_slashes(s)
CreateMultipartUpload_dir_obj(s)
PutObject_name_too_long(s)
HeadObject_name_too_long(s)
@@ -872,6 +875,12 @@ func TestVersioningDisabled(s *S3Conf) {
VersioningDisabled_PutBucketVersioning_not_configured(s)
}
+func TestRouter(s *S3Conf) {
+ RouterPutPartNumberWithoutUploadId(s)
+ RouterPostRoot(s)
+ RouterPostObjectWithoutQuery(s)
+}
+
type IntTests map[string]func(s *S3Conf) error
func GetIntTests() IntTests {
@@ -917,6 +926,7 @@ func GetIntTests() IntTests {
"PutObject_with_object_lock": PutObject_with_object_lock,
"PutObject_invalid_legal_hold": PutObject_invalid_legal_hold,
"PutObject_invalid_object_lock_mode": PutObject_invalid_object_lock_mode,
+ "PutObject_invalid_credentials": PutObject_invalid_credentials,
"PutObject_checksum_algorithm_and_header_mismatch": PutObject_checksum_algorithm_and_header_mismatch,
"PutObject_multiple_checksum_headers": PutObject_multiple_checksum_headers,
"PutObject_invalid_checksum_header": PutObject_invalid_checksum_header,
@@ -1273,6 +1283,7 @@ func GetIntTests() IntTests {
"PutObject_overwrite_file_obj": PutObject_overwrite_file_obj,
"PutObject_overwrite_file_obj_with_nested_obj": PutObject_overwrite_file_obj_with_nested_obj,
"PutObject_dir_obj_with_data": PutObject_dir_obj_with_data,
+ "PutObject_with_slashes": PutObject_with_slashes,
"CreateMultipartUpload_dir_obj": CreateMultipartUpload_dir_obj,
"IAM_user_access_denied": IAM_user_access_denied,
"IAM_userplus_access_denied": IAM_userplus_access_denied,
@@ -1354,5 +1365,9 @@ func GetIntTests() IntTests {
"Versioning_WORM_obj_version_locked_with_governance_retention": Versioning_WORM_obj_version_locked_with_governance_retention,
"Versioning_WORM_obj_version_locked_with_compliance_retention": Versioning_WORM_obj_version_locked_with_compliance_retention,
"Versioning_concurrent_upload_object": Versioning_concurrent_upload_object,
+ "RouterPutPartNumberWithoutUploadId": RouterPutPartNumberWithoutUploadId,
+ "RouterPostRoot": RouterPostRoot,
+ "RouterPostObjectWithoutQuery": RouterPostObjectWithoutQuery,
+ "RouterPUTObjectOnlyUploadId": RouterPUTObjectOnlyUploadId,
}
}
diff --git a/tests/integration/s3conf.go b/tests/integration/s3conf.go
index 19b8d269..9b965b77 100644
--- a/tests/integration/s3conf.go
+++ b/tests/integration/s3conf.go
@@ -130,6 +130,10 @@ func (c *S3Conf) GetClient() *s3.Client {
})
}
+func (c *S3Conf) GetPresignClient() *s3.PresignClient {
+ return s3.NewPresignClient(c.GetClient())
+}
+
func (c *S3Conf) GetAnonymousClient() *s3.Client {
cfg := c.Config()
cfg.Credentials = aws.AnonymousCredentials{}
diff --git a/tests/integration/tests.go b/tests/integration/tests.go
index 74345044..f2e1e023 100644
--- a/tests/integration/tests.go
+++ b/tests/integration/tests.go
@@ -51,7 +51,6 @@ func Authentication_invalid_auth_header(s *S3Conf) error {
testName := "Authentication_invalid_auth_header"
return authHandler(s, &authConfig{
testName: testName,
- path: "my-bucket",
method: http.MethodGet,
body: nil,
service: "s3",
@@ -64,7 +63,7 @@ func Authentication_invalid_auth_header(s *S3Conf) error {
return err
}
defer resp.Body.Close()
- if err := checkAuthErr(resp, s3err.GetAPIError(s3err.ErrMissingFields)); err != nil {
+ if err := checkHTTPResponseApiErr(resp, s3err.GetAPIError(s3err.ErrMissingFields)); err != nil {
return err
}
@@ -76,7 +75,6 @@ func Authentication_unsupported_signature_version(s *S3Conf) error {
testName := "Authentication_unsupported_signature_version"
return authHandler(s, &authConfig{
testName: testName,
- path: "my-bucket",
method: http.MethodGet,
body: nil,
service: "s3",
@@ -91,7 +89,7 @@ func Authentication_unsupported_signature_version(s *S3Conf) error {
return err
}
defer resp.Body.Close()
- if err := checkAuthErr(resp, s3err.GetAPIError(s3err.ErrSignatureVersionNotSupported)); err != nil {
+ if err := checkHTTPResponseApiErr(resp, s3err.GetAPIError(s3err.ErrSignatureVersionNotSupported)); err != nil {
return err
}
@@ -103,7 +101,6 @@ func Authentication_malformed_credentials(s *S3Conf) error {
testName := "Authentication_malformed_credentials"
return authHandler(s, &authConfig{
testName: testName,
- path: "my-bucket",
method: http.MethodGet,
body: nil,
service: "s3",
@@ -119,7 +116,7 @@ func Authentication_malformed_credentials(s *S3Conf) error {
return err
}
defer resp.Body.Close()
- if err := checkAuthErr(resp, s3err.GetAPIError(s3err.ErrCredMalformed)); err != nil {
+ if err := checkHTTPResponseApiErr(resp, s3err.GetAPIError(s3err.ErrCredMalformed)); err != nil {
return err
}
@@ -131,7 +128,6 @@ func Authentication_malformed_credentials_invalid_parts(s *S3Conf) error {
testName := "Authentication_malformed_credentials_invalid_parts"
return authHandler(s, &authConfig{
testName: testName,
- path: "my-bucket",
method: http.MethodGet,
body: nil,
service: "s3",
@@ -147,7 +143,7 @@ func Authentication_malformed_credentials_invalid_parts(s *S3Conf) error {
return err
}
defer resp.Body.Close()
- if err := checkAuthErr(resp, s3err.GetAPIError(s3err.ErrCredMalformed)); err != nil {
+ if err := checkHTTPResponseApiErr(resp, s3err.GetAPIError(s3err.ErrCredMalformed)); err != nil {
return err
}
@@ -159,7 +155,6 @@ func Authentication_credentials_terminated_string(s *S3Conf) error {
testName := "Authentication_credentials_terminated_string"
return authHandler(s, &authConfig{
testName: testName,
- path: "my-bucket",
method: http.MethodGet,
body: nil,
service: "s3",
@@ -175,7 +170,7 @@ func Authentication_credentials_terminated_string(s *S3Conf) error {
return err
}
defer resp.Body.Close()
- if err := checkAuthErr(resp, s3err.GetAPIError(s3err.ErrSignatureTerminationStr)); err != nil {
+ if err := checkHTTPResponseApiErr(resp, s3err.GetAPIError(s3err.ErrSignatureTerminationStr)); err != nil {
return err
}
@@ -187,7 +182,6 @@ func Authentication_credentials_incorrect_service(s *S3Conf) error {
testName := "Authentication_credentials_incorrect_service"
return authHandler(s, &authConfig{
testName: testName,
- path: "my-bucket",
method: http.MethodGet,
body: nil,
service: "ec2",
@@ -199,7 +193,7 @@ func Authentication_credentials_incorrect_service(s *S3Conf) error {
return err
}
defer resp.Body.Close()
- if err := checkAuthErr(resp, s3err.GetAPIError(s3err.ErrSignatureIncorrService)); err != nil {
+ if err := checkHTTPResponseApiErr(resp, s3err.GetAPIError(s3err.ErrSignatureIncorrService)); err != nil {
return err
}
@@ -217,7 +211,6 @@ func Authentication_credentials_incorrect_region(s *S3Conf) error {
}
return authHandler(&cfg, &authConfig{
testName: testName,
- path: "my-bucket",
method: http.MethodGet,
body: nil,
service: "s3",
@@ -234,7 +227,7 @@ func Authentication_credentials_incorrect_region(s *S3Conf) error {
return err
}
defer resp.Body.Close()
- if err := checkAuthErr(resp, apiErr); err != nil {
+ if err := checkHTTPResponseApiErr(resp, apiErr); err != nil {
return err
}
@@ -246,7 +239,6 @@ func Authentication_credentials_invalid_date(s *S3Conf) error {
testName := "Authentication_credentials_invalid_date"
return authHandler(s, &authConfig{
testName: testName,
- path: "my-bucket",
method: http.MethodGet,
body: nil,
service: "s3",
@@ -262,7 +254,7 @@ func Authentication_credentials_invalid_date(s *S3Conf) error {
return err
}
defer resp.Body.Close()
- if err := checkAuthErr(resp, s3err.GetAPIError(s3err.ErrSignatureDateDoesNotMatch)); err != nil {
+ if err := checkHTTPResponseApiErr(resp, s3err.GetAPIError(s3err.ErrSignatureDateDoesNotMatch)); err != nil {
return err
}
@@ -274,7 +266,6 @@ func Authentication_credentials_future_date(s *S3Conf) error {
testName := "Authentication_credentials_future_date"
return authHandler(s, &authConfig{
testName: testName,
- path: "my-bucket",
method: http.MethodGet,
body: nil,
service: "s3",
@@ -312,7 +303,6 @@ func Authentication_credentials_past_date(s *S3Conf) error {
testName := "Authentication_credentials_past_date"
return authHandler(s, &authConfig{
testName: testName,
- path: "my-bucket",
method: http.MethodGet,
body: nil,
service: "s3",
@@ -350,7 +340,6 @@ func Authentication_credentials_non_existing_access_key(s *S3Conf) error {
testName := "Authentication_credentials_non_existing_access_key"
return authHandler(s, &authConfig{
testName: testName,
- path: "my-bucket",
method: http.MethodGet,
body: nil,
service: "s3",
@@ -366,7 +355,7 @@ func Authentication_credentials_non_existing_access_key(s *S3Conf) error {
return err
}
defer resp.Body.Close()
- if err := checkAuthErr(resp, s3err.GetAPIError(s3err.ErrInvalidAccessKeyID)); err != nil {
+ if err := checkHTTPResponseApiErr(resp, s3err.GetAPIError(s3err.ErrInvalidAccessKeyID)); err != nil {
return err
}
@@ -378,7 +367,6 @@ func Authentication_invalid_signed_headers(s *S3Conf) error {
testName := "Authentication_invalid_signed_headers"
return authHandler(s, &authConfig{
testName: testName,
- path: "my-bucket",
method: http.MethodGet,
body: nil,
service: "s3",
@@ -394,7 +382,7 @@ func Authentication_invalid_signed_headers(s *S3Conf) error {
return err
}
defer resp.Body.Close()
- if err := checkAuthErr(resp, s3err.GetAPIError(s3err.ErrInvalidQueryParams)); err != nil {
+ if err := checkHTTPResponseApiErr(resp, s3err.GetAPIError(s3err.ErrInvalidQueryParams)); err != nil {
return err
}
@@ -406,7 +394,6 @@ func Authentication_missing_date_header(s *S3Conf) error {
testName := "Authentication_missing_date_header"
return authHandler(s, &authConfig{
testName: testName,
- path: "my-bucket",
method: http.MethodGet,
body: nil,
service: "s3",
@@ -419,7 +406,7 @@ func Authentication_missing_date_header(s *S3Conf) error {
return err
}
defer resp.Body.Close()
- if err := checkAuthErr(resp, s3err.GetAPIError(s3err.ErrMissingDateHeader)); err != nil {
+ if err := checkHTTPResponseApiErr(resp, s3err.GetAPIError(s3err.ErrMissingDateHeader)); err != nil {
return err
}
@@ -431,7 +418,6 @@ func Authentication_invalid_date_header(s *S3Conf) error {
testName := "Authentication_invalid_date_header"
return authHandler(s, &authConfig{
testName: testName,
- path: "my-bucket",
method: http.MethodGet,
body: nil,
service: "s3",
@@ -444,7 +430,7 @@ func Authentication_invalid_date_header(s *S3Conf) error {
return err
}
defer resp.Body.Close()
- if err := checkAuthErr(resp, s3err.GetAPIError(s3err.ErrMalformedDate)); err != nil {
+ if err := checkHTTPResponseApiErr(resp, s3err.GetAPIError(s3err.ErrMalformedDate)); err != nil {
return err
}
@@ -456,7 +442,6 @@ func Authentication_date_mismatch(s *S3Conf) error {
testName := "Authentication_date_mismatch"
return authHandler(s, &authConfig{
testName: testName,
- path: "my-bucket",
method: http.MethodGet,
body: nil,
service: "s3",
@@ -469,7 +454,7 @@ func Authentication_date_mismatch(s *S3Conf) error {
return err
}
defer resp.Body.Close()
- if err := checkAuthErr(resp, s3err.GetAPIError(s3err.ErrSignatureDateDoesNotMatch)); err != nil {
+ if err := checkHTTPResponseApiErr(resp, s3err.GetAPIError(s3err.ErrSignatureDateDoesNotMatch)); err != nil {
return err
}
@@ -481,7 +466,6 @@ func Authentication_incorrect_payload_hash(s *S3Conf) error {
testName := "Authentication_incorrect_payload_hash"
return authHandler(s, &authConfig{
testName: testName,
- path: "my-bucket",
method: http.MethodGet,
body: nil,
service: "s3",
@@ -494,7 +478,7 @@ func Authentication_incorrect_payload_hash(s *S3Conf) error {
return err
}
defer resp.Body.Close()
- if err := checkAuthErr(resp, s3err.GetAPIError(s3err.ErrContentSHA256Mismatch)); err != nil {
+ if err := checkHTTPResponseApiErr(resp, s3err.GetAPIError(s3err.ErrContentSHA256Mismatch)); err != nil {
return err
}
@@ -506,7 +490,6 @@ func Authentication_incorrect_md5(s *S3Conf) error {
testName := "Authentication_incorrect_md5"
return authHandler(s, &authConfig{
testName: testName,
- path: "my-bucket",
method: http.MethodGet,
body: nil,
service: "s3",
@@ -520,7 +503,7 @@ func Authentication_incorrect_md5(s *S3Conf) error {
return err
}
defer resp.Body.Close()
- if err := checkAuthErr(resp, s3err.GetAPIError(s3err.ErrInvalidDigest)); err != nil {
+ if err := checkHTTPResponseApiErr(resp, s3err.GetAPIError(s3err.ErrInvalidDigest)); err != nil {
return err
}
@@ -534,7 +517,6 @@ func Authentication_signature_error_incorrect_secret_key(s *S3Conf) error {
cfg.awsSecret = s.awsSecret + "a"
return authHandler(&cfg, &authConfig{
testName: testName,
- path: "my-bucket",
method: http.MethodGet,
body: nil,
service: "s3",
@@ -546,7 +528,7 @@ func Authentication_signature_error_incorrect_secret_key(s *S3Conf) error {
return err
}
defer resp.Body.Close()
- if err := checkAuthErr(resp, s3err.GetAPIError(s3err.ErrSignatureDoesNotMatch)); err != nil {
+ if err := checkHTTPResponseApiErr(resp, s3err.GetAPIError(s3err.ErrSignatureDoesNotMatch)); err != nil {
return err
}
@@ -556,9 +538,9 @@ func Authentication_signature_error_incorrect_secret_key(s *S3Conf) error {
func PresignedAuth_unsupported_algorithm(s *S3Conf) error {
testName := "PresignedAuth_unsupported_algorithm"
- return presignedAuthHandler(s, testName, func(client *s3.PresignClient) error {
+ return presignedAuthHandler(s, testName, func(client *s3.PresignClient, bucket string) error {
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
- v4req, err := client.PresignDeleteBucket(ctx, &s3.DeleteBucketInput{Bucket: getPtr("my-bucket")})
+ v4req, err := client.PresignDeleteBucket(ctx, &s3.DeleteBucketInput{Bucket: &bucket})
cancel()
if err != nil {
return err
@@ -576,7 +558,7 @@ func PresignedAuth_unsupported_algorithm(s *S3Conf) error {
return err
}
- if err := checkAuthErr(resp, s3err.GetAPIError(s3err.ErrInvalidQuerySignatureAlgo)); err != nil {
+ if err := checkHTTPResponseApiErr(resp, s3err.GetAPIError(s3err.ErrInvalidQuerySignatureAlgo)); err != nil {
return err
}
@@ -586,9 +568,9 @@ func PresignedAuth_unsupported_algorithm(s *S3Conf) error {
func PresignedAuth_missing_credentials_query_param(s *S3Conf) error {
testName := "PresignedAuth_missing_credentials_query_param"
- return presignedAuthHandler(s, testName, func(client *s3.PresignClient) error {
+ return presignedAuthHandler(s, testName, func(client *s3.PresignClient, bucket string) error {
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
- v4req, err := client.PresignDeleteBucket(ctx, &s3.DeleteBucketInput{Bucket: getPtr("my-bucket")})
+ v4req, err := client.PresignDeleteBucket(ctx, &s3.DeleteBucketInput{Bucket: &bucket})
cancel()
if err != nil {
return err
@@ -613,7 +595,7 @@ func PresignedAuth_missing_credentials_query_param(s *S3Conf) error {
return err
}
- if err := checkAuthErr(resp, s3err.GetAPIError(s3err.ErrInvalidQueryParams)); err != nil {
+ if err := checkHTTPResponseApiErr(resp, s3err.GetAPIError(s3err.ErrInvalidQueryParams)); err != nil {
return err
}
@@ -623,9 +605,9 @@ func PresignedAuth_missing_credentials_query_param(s *S3Conf) error {
func PresignedAuth_malformed_creds_invalid_parts(s *S3Conf) error {
testName := "PresignedAuth_malformed_creds_invalid_parts"
- return presignedAuthHandler(s, testName, func(client *s3.PresignClient) error {
+ return presignedAuthHandler(s, testName, func(client *s3.PresignClient, bucket string) error {
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
- v4req, err := client.PresignDeleteBucket(ctx, &s3.DeleteBucketInput{Bucket: getPtr("my-bucket")})
+ v4req, err := client.PresignDeleteBucket(ctx, &s3.DeleteBucketInput{Bucket: &bucket})
cancel()
if err != nil {
return err
@@ -650,7 +632,7 @@ func PresignedAuth_malformed_creds_invalid_parts(s *S3Conf) error {
return err
}
- if err := checkAuthErr(resp, s3err.GetAPIError(s3err.ErrCredMalformed)); err != nil {
+ if err := checkHTTPResponseApiErr(resp, s3err.GetAPIError(s3err.ErrCredMalformed)); err != nil {
return err
}
@@ -660,9 +642,9 @@ func PresignedAuth_malformed_creds_invalid_parts(s *S3Conf) error {
func PresignedAuth_creds_invalid_terminator(s *S3Conf) error {
testName := "PresignedAuth_creds_invalid_terminator"
- return presignedAuthHandler(s, testName, func(client *s3.PresignClient) error {
+ return presignedAuthHandler(s, testName, func(client *s3.PresignClient, bucket string) error {
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
- v4req, err := client.PresignDeleteBucket(ctx, &s3.DeleteBucketInput{Bucket: getPtr("my-bucket")})
+ v4req, err := client.PresignDeleteBucket(ctx, &s3.DeleteBucketInput{Bucket: &bucket})
cancel()
if err != nil {
return err
@@ -683,7 +665,7 @@ func PresignedAuth_creds_invalid_terminator(s *S3Conf) error {
return err
}
- if err := checkAuthErr(resp, s3err.GetAPIError(s3err.ErrSignatureTerminationStr)); err != nil {
+ if err := checkHTTPResponseApiErr(resp, s3err.GetAPIError(s3err.ErrSignatureTerminationStr)); err != nil {
return err
}
@@ -693,9 +675,9 @@ func PresignedAuth_creds_invalid_terminator(s *S3Conf) error {
func PresignedAuth_creds_incorrect_service(s *S3Conf) error {
testName := "PresignedAuth_creds_incorrect_service"
- return presignedAuthHandler(s, testName, func(client *s3.PresignClient) error {
+ return presignedAuthHandler(s, testName, func(client *s3.PresignClient, bucket string) error {
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
- v4req, err := client.PresignDeleteBucket(ctx, &s3.DeleteBucketInput{Bucket: getPtr("my-bucket")})
+ v4req, err := client.PresignDeleteBucket(ctx, &s3.DeleteBucketInput{Bucket: &bucket})
cancel()
if err != nil {
return err
@@ -716,7 +698,7 @@ func PresignedAuth_creds_incorrect_service(s *S3Conf) error {
return err
}
- if err := checkAuthErr(resp, s3err.GetAPIError(s3err.ErrSignatureIncorrService)); err != nil {
+ if err := checkHTTPResponseApiErr(resp, s3err.GetAPIError(s3err.ErrSignatureIncorrService)); err != nil {
return err
}
@@ -726,17 +708,19 @@ func PresignedAuth_creds_incorrect_service(s *S3Conf) error {
func PresignedAuth_creds_incorrect_region(s *S3Conf) error {
testName := "PresignedAuth_creds_incorrect_region"
- cfg := *s
+ return presignedAuthHandler(s, testName, func(_ *s3.PresignClient, bucket string) error {
+ cfg := *s
- if cfg.awsRegion == "us-east-1" {
- cfg.awsRegion = "us-west-1"
- } else {
- cfg.awsRegion = "us-east-1"
- }
+ if cfg.awsRegion == "us-east-1" {
+ cfg.awsRegion = "us-west-1"
+ } else {
+ cfg.awsRegion = "us-east-1"
+ }
+
+ client := cfg.GetPresignClient()
- return presignedAuthHandler(&cfg, testName, func(client *s3.PresignClient) error {
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
- v4req, err := client.PresignDeleteBucket(ctx, &s3.DeleteBucketInput{Bucket: getPtr("my-bucket")})
+ v4req, err := client.PresignDeleteBucket(ctx, &s3.DeleteBucketInput{Bucket: &bucket})
cancel()
if err != nil {
return err
@@ -752,7 +736,7 @@ func PresignedAuth_creds_incorrect_region(s *S3Conf) error {
return err
}
- if err := checkAuthErr(resp, s3err.APIError{
+ if err := checkHTTPResponseApiErr(resp, s3err.APIError{
Code: "SignatureDoesNotMatch",
Description: fmt.Sprintf("Credential should be scoped to a valid Region, not %v", cfg.awsRegion),
HTTPStatusCode: http.StatusForbidden,
@@ -766,9 +750,9 @@ func PresignedAuth_creds_incorrect_region(s *S3Conf) error {
func PresignedAuth_creds_invalid_date(s *S3Conf) error {
testName := "PresignedAuth_creds_invalid_date"
- return presignedAuthHandler(s, testName, func(client *s3.PresignClient) error {
+ return presignedAuthHandler(s, testName, func(client *s3.PresignClient, bucket string) error {
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
- v4req, err := client.PresignDeleteBucket(ctx, &s3.DeleteBucketInput{Bucket: getPtr("my-bucket")})
+ v4req, err := client.PresignDeleteBucket(ctx, &s3.DeleteBucketInput{Bucket: &bucket})
cancel()
if err != nil {
return err
@@ -789,7 +773,7 @@ func PresignedAuth_creds_invalid_date(s *S3Conf) error {
return err
}
- if err := checkAuthErr(resp, s3err.GetAPIError(s3err.ErrSignatureDateDoesNotMatch)); err != nil {
+ if err := checkHTTPResponseApiErr(resp, s3err.GetAPIError(s3err.ErrSignatureDateDoesNotMatch)); err != nil {
return err
}
@@ -799,9 +783,9 @@ func PresignedAuth_creds_invalid_date(s *S3Conf) error {
func PresignedAuth_non_existing_access_key_id(s *S3Conf) error {
testName := "PresignedAuth_non_existing_access_key_id"
- return presignedAuthHandler(s, testName, func(client *s3.PresignClient) error {
+ return presignedAuthHandler(s, testName, func(client *s3.PresignClient, bucket string) error {
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
- v4req, err := client.PresignDeleteBucket(ctx, &s3.DeleteBucketInput{Bucket: getPtr("my-bucket")})
+ v4req, err := client.PresignDeleteBucket(ctx, &s3.DeleteBucketInput{Bucket: &bucket})
cancel()
if err != nil {
return err
@@ -822,7 +806,7 @@ func PresignedAuth_non_existing_access_key_id(s *S3Conf) error {
return err
}
- if err := checkAuthErr(resp, s3err.GetAPIError(s3err.ErrInvalidAccessKeyID)); err != nil {
+ if err := checkHTTPResponseApiErr(resp, s3err.GetAPIError(s3err.ErrInvalidAccessKeyID)); err != nil {
return err
}
@@ -832,9 +816,9 @@ func PresignedAuth_non_existing_access_key_id(s *S3Conf) error {
func PresignedAuth_missing_date_query(s *S3Conf) error {
testName := "PresignedAuth_missing_date_query"
- return presignedAuthHandler(s, testName, func(client *s3.PresignClient) error {
+ return presignedAuthHandler(s, testName, func(client *s3.PresignClient, bucket string) error {
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
- v4req, err := client.PresignDeleteBucket(ctx, &s3.DeleteBucketInput{Bucket: getPtr("my-bucket")})
+ v4req, err := client.PresignDeleteBucket(ctx, &s3.DeleteBucketInput{Bucket: &bucket})
cancel()
if err != nil {
return err
@@ -859,7 +843,7 @@ func PresignedAuth_missing_date_query(s *S3Conf) error {
return err
}
- if err := checkAuthErr(resp, s3err.GetAPIError(s3err.ErrInvalidQueryParams)); err != nil {
+ if err := checkHTTPResponseApiErr(resp, s3err.GetAPIError(s3err.ErrInvalidQueryParams)); err != nil {
return err
}
@@ -869,9 +853,9 @@ func PresignedAuth_missing_date_query(s *S3Conf) error {
func PresignedAuth_dates_mismatch(s *S3Conf) error {
testName := "PresignedAuth_dates_mismatch"
- return presignedAuthHandler(s, testName, func(client *s3.PresignClient) error {
+ return presignedAuthHandler(s, testName, func(client *s3.PresignClient, bucket string) error {
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
- v4req, err := client.PresignDeleteBucket(ctx, &s3.DeleteBucketInput{Bucket: getPtr("my-bucket")})
+ v4req, err := client.PresignDeleteBucket(ctx, &s3.DeleteBucketInput{Bucket: &bucket})
cancel()
if err != nil {
return err
@@ -892,7 +876,7 @@ func PresignedAuth_dates_mismatch(s *S3Conf) error {
return err
}
- if err := checkAuthErr(resp, s3err.GetAPIError(s3err.ErrSignatureDateDoesNotMatch)); err != nil {
+ if err := checkHTTPResponseApiErr(resp, s3err.GetAPIError(s3err.ErrSignatureDateDoesNotMatch)); err != nil {
return err
}
@@ -902,9 +886,9 @@ func PresignedAuth_dates_mismatch(s *S3Conf) error {
func PresignedAuth_missing_signed_headers_query_param(s *S3Conf) error {
testName := "PresignedAuth_missing_signed_headers_query_param"
- return presignedAuthHandler(s, testName, func(client *s3.PresignClient) error {
+ return presignedAuthHandler(s, testName, func(client *s3.PresignClient, bucket string) error {
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
- v4req, err := client.PresignDeleteBucket(ctx, &s3.DeleteBucketInput{Bucket: getPtr("my-bucket")})
+ v4req, err := client.PresignDeleteBucket(ctx, &s3.DeleteBucketInput{Bucket: &bucket})
cancel()
if err != nil {
return err
@@ -929,7 +913,7 @@ func PresignedAuth_missing_signed_headers_query_param(s *S3Conf) error {
return err
}
- if err := checkAuthErr(resp, s3err.GetAPIError(s3err.ErrInvalidQueryParams)); err != nil {
+ if err := checkHTTPResponseApiErr(resp, s3err.GetAPIError(s3err.ErrInvalidQueryParams)); err != nil {
return err
}
@@ -939,9 +923,9 @@ func PresignedAuth_missing_signed_headers_query_param(s *S3Conf) error {
func PresignedAuth_missing_expiration_query_param(s *S3Conf) error {
testName := "PresignedAuth_missing_expiration_query_param"
- return presignedAuthHandler(s, testName, func(client *s3.PresignClient) error {
+ return presignedAuthHandler(s, testName, func(client *s3.PresignClient, bucket string) error {
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
- v4req, err := client.PresignDeleteBucket(ctx, &s3.DeleteBucketInput{Bucket: getPtr("my-bucket")})
+ v4req, err := client.PresignDeleteBucket(ctx, &s3.DeleteBucketInput{Bucket: &bucket})
cancel()
if err != nil {
return err
@@ -966,7 +950,7 @@ func PresignedAuth_missing_expiration_query_param(s *S3Conf) error {
return err
}
- if err := checkAuthErr(resp, s3err.GetAPIError(s3err.ErrInvalidQueryParams)); err != nil {
+ if err := checkHTTPResponseApiErr(resp, s3err.GetAPIError(s3err.ErrInvalidQueryParams)); err != nil {
return err
}
@@ -976,9 +960,9 @@ func PresignedAuth_missing_expiration_query_param(s *S3Conf) error {
func PresignedAuth_invalid_expiration_query_param(s *S3Conf) error {
testName := "PresignedAuth_invalid_expiration_query_param"
- return presignedAuthHandler(s, testName, func(client *s3.PresignClient) error {
+ return presignedAuthHandler(s, testName, func(client *s3.PresignClient, bucket string) error {
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
- v4req, err := client.PresignDeleteBucket(ctx, &s3.DeleteBucketInput{Bucket: getPtr("my-bucket")})
+ v4req, err := client.PresignDeleteBucket(ctx, &s3.DeleteBucketInput{Bucket: &bucket})
cancel()
if err != nil {
return err
@@ -1003,7 +987,7 @@ func PresignedAuth_invalid_expiration_query_param(s *S3Conf) error {
return err
}
- if err := checkAuthErr(resp, s3err.GetAPIError(s3err.ErrMalformedExpires)); err != nil {
+ if err := checkHTTPResponseApiErr(resp, s3err.GetAPIError(s3err.ErrMalformedExpires)); err != nil {
return err
}
@@ -1013,9 +997,9 @@ func PresignedAuth_invalid_expiration_query_param(s *S3Conf) error {
func PresignedAuth_negative_expiration_query_param(s *S3Conf) error {
testName := "PresignedAuth_negative_expiration_query_param"
- return presignedAuthHandler(s, testName, func(client *s3.PresignClient) error {
+ return presignedAuthHandler(s, testName, func(client *s3.PresignClient, bucket string) error {
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
- v4req, err := client.PresignDeleteBucket(ctx, &s3.DeleteBucketInput{Bucket: getPtr("my-bucket")})
+ v4req, err := client.PresignDeleteBucket(ctx, &s3.DeleteBucketInput{Bucket: &bucket})
cancel()
if err != nil {
return err
@@ -1040,7 +1024,7 @@ func PresignedAuth_negative_expiration_query_param(s *S3Conf) error {
return err
}
- if err := checkAuthErr(resp, s3err.GetAPIError(s3err.ErrNegativeExpires)); err != nil {
+ if err := checkHTTPResponseApiErr(resp, s3err.GetAPIError(s3err.ErrNegativeExpires)); err != nil {
return err
}
@@ -1050,9 +1034,9 @@ func PresignedAuth_negative_expiration_query_param(s *S3Conf) error {
func PresignedAuth_exceeding_expiration_query_param(s *S3Conf) error {
testName := "PresignedAuth_exceeding_expiration_query_param"
- return presignedAuthHandler(s, testName, func(client *s3.PresignClient) error {
+ return presignedAuthHandler(s, testName, func(client *s3.PresignClient, bucket string) error {
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
- v4req, err := client.PresignDeleteBucket(ctx, &s3.DeleteBucketInput{Bucket: getPtr("my-bucket")})
+ v4req, err := client.PresignDeleteBucket(ctx, &s3.DeleteBucketInput{Bucket: &bucket})
cancel()
if err != nil {
return err
@@ -1077,7 +1061,7 @@ func PresignedAuth_exceeding_expiration_query_param(s *S3Conf) error {
return err
}
- if err := checkAuthErr(resp, s3err.GetAPIError(s3err.ErrMaximumExpires)); err != nil {
+ if err := checkHTTPResponseApiErr(resp, s3err.GetAPIError(s3err.ErrMaximumExpires)); err != nil {
return err
}
@@ -1087,9 +1071,9 @@ func PresignedAuth_exceeding_expiration_query_param(s *S3Conf) error {
func PresignedAuth_expired_request(s *S3Conf) error {
testName := "PresignedAuth_expired_request"
- return presignedAuthHandler(s, testName, func(client *s3.PresignClient) error {
+ return presignedAuthHandler(s, testName, func(client *s3.PresignClient, bucket string) error {
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
- v4req, err := client.PresignDeleteBucket(ctx, &s3.DeleteBucketInput{Bucket: getPtr("my-bucket")})
+ v4req, err := client.PresignDeleteBucket(ctx, &s3.DeleteBucketInput{Bucket: &bucket})
cancel()
if err != nil {
return err
@@ -1121,7 +1105,7 @@ func PresignedAuth_expired_request(s *S3Conf) error {
return err
}
- if err := checkAuthErr(resp, s3err.GetAPIError(s3err.ErrExpiredPresignRequest)); err != nil {
+ if err := checkHTTPResponseApiErr(resp, s3err.GetAPIError(s3err.ErrExpiredPresignRequest)); err != nil {
return err
}
@@ -1131,11 +1115,13 @@ func PresignedAuth_expired_request(s *S3Conf) error {
func PresignedAuth_incorrect_secret_key(s *S3Conf) error {
testName := "PresignedAuth_incorrect_secret_key"
- cfg := *s
- cfg.awsSecret += "x"
- return presignedAuthHandler(&cfg, testName, func(client *s3.PresignClient) error {
+ return presignedAuthHandler(s, testName, func(_ *s3.PresignClient, bucket string) error {
+ cfg := *s
+ cfg.awsSecret += "x"
+ client := cfg.GetPresignClient()
+
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
- v4req, err := client.PresignDeleteBucket(ctx, &s3.DeleteBucketInput{Bucket: getPtr("my-bucket")})
+ v4req, err := client.PresignDeleteBucket(ctx, &s3.DeleteBucketInput{Bucket: &bucket})
cancel()
if err != nil {
return err
@@ -1151,7 +1137,7 @@ func PresignedAuth_incorrect_secret_key(s *S3Conf) error {
return err
}
- if err := checkAuthErr(resp, s3err.GetAPIError(s3err.ErrSignatureDoesNotMatch)); err != nil {
+ if err := checkHTTPResponseApiErr(resp, s3err.GetAPIError(s3err.ErrSignatureDoesNotMatch)); err != nil {
return err
}
@@ -1161,12 +1147,7 @@ func PresignedAuth_incorrect_secret_key(s *S3Conf) error {
func PresignedAuth_PutObject_success(s *S3Conf) error {
testName := "PresignedAuth_PutObject_success"
- return presignedAuthHandler(s, testName, func(client *s3.PresignClient) error {
- bucket := getBucketName()
- err := setup(s, bucket)
- if err != nil {
- return err
- }
+ return presignedAuthHandler(s, testName, func(client *s3.PresignClient, bucket string) error {
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
v4req, err := client.PresignPutObject(ctx, &s3.PutObjectInput{Bucket: &bucket, Key: getPtr("my-obj")})
cancel()
@@ -1188,23 +1169,14 @@ func PresignedAuth_PutObject_success(s *S3Conf) error {
return fmt.Errorf("expected my-obj to be successfully uploaded and get 200 response status, instead got %v", resp.StatusCode)
}
- err = teardown(s, bucket)
- if err != nil {
- return err
- }
-
return nil
})
}
func PresignedAuth_Put_GetObject_with_data(s *S3Conf) error {
testName := "PresignedAuth_Put_GetObject_with_data"
- return presignedAuthHandler(s, testName, func(client *s3.PresignClient) error {
- bucket, obj := getBucketName(), "my-obj"
- err := setup(s, bucket)
- if err != nil {
- return err
- }
+ return presignedAuthHandler(s, testName, func(client *s3.PresignClient, bucket string) error {
+ obj := "my-obj"
data := "Hello world"
body := strings.NewReader(data)
@@ -1263,23 +1235,14 @@ func PresignedAuth_Put_GetObject_with_data(s *S3Conf) error {
return fmt.Errorf("expected get object response body to be %v, instead got %s", data, respBody)
}
- err = teardown(s, bucket)
- if err != nil {
- return err
- }
-
return nil
})
}
func PresignedAuth_Put_GetObject_with_UTF8_chars(s *S3Conf) error {
testName := "PresignedAuth_Put_GetObject_with_UTF8_chars"
- return presignedAuthHandler(s, testName, func(client *s3.PresignClient) error {
- bucket, obj := getBucketName(), "my-$%^&*;"
- err := setup(s, bucket)
- if err != nil {
- return err
- }
+ return presignedAuthHandler(s, testName, func(client *s3.PresignClient, bucket string) error {
+ obj := "my-$%^&*;"
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
v4req, err := client.PresignPutObject(ctx, &s3.PutObjectInput{Bucket: &bucket, Key: &obj})
@@ -1325,24 +1288,14 @@ func PresignedAuth_Put_GetObject_with_UTF8_chars(s *S3Conf) error {
return fmt.Errorf("expected get object response status to be %v, instead got %v", http.StatusOK, resp.StatusCode)
}
- err = teardown(s, bucket)
- if err != nil {
- return err
- }
-
return nil
})
}
func PresignedAuth_UploadPart(s *S3Conf) error {
testName := "PresignedAuth_UploadPart"
- return presignedAuthHandler(s, testName, func(client *s3.PresignClient) error {
- bucket, key, partNumber := getBucketName(), "my-mp", int32(1)
-
- err := setup(s, bucket)
- if err != nil {
- return err
- }
+ return presignedAuthHandler(s, testName, func(client *s3.PresignClient, bucket string) error {
+ key, partNumber := "my-mp", int32(1)
clt := s.GetClient()
mp, err := createMp(clt, bucket, key)
@@ -1393,11 +1346,6 @@ func PresignedAuth_UploadPart(s *S3Conf) error {
return fmt.Errorf("expected uploaded part part-number to be %v, instead got %v", partNumber, *out.Parts[0].PartNumber)
}
- err = teardown(s, bucket)
- if err != nil {
- return err
- }
-
return nil
})
}
@@ -11312,7 +11260,7 @@ func CompleteMultipartUpload_mpu_object_size(s *S3Conf) error {
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
_, err = s3client.CompleteMultipartUpload(ctx, input)
cancel()
- if err := checkApiErr(err, s3err.GetInvalidMpObjectSizeErr(invMpuSize)); err != nil {
+ if err := checkApiErr(err, s3err.GetNegatvieMpObjectSizeErr(invMpuSize)); err != nil {
return err
}
@@ -17445,7 +17393,7 @@ func PublicBucket_public_acl(s *S3Conf) error {
})
return err
},
- ExpectedErr: s3err.GetAPIError(s3err.ErrNotImplemented),
+ ExpectedErr: s3err.GetAPIError(s3err.ErrAccessDenied),
},
{
Action: "GetObjectAttributes",
@@ -17996,6 +17944,47 @@ func PutObject_dir_obj_with_data(s *S3Conf) error {
})
}
+func PutObject_with_slashes(s *S3Conf) error {
+ testName := "PutObject_with_slashes"
+ return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
+ objs, err := putObjects(s3client, []string{
+ "/obj", "foo//bar", "/foo/baz/bar", "////////bar", "foo//////quxx",
+ }, bucket)
+ if err != nil {
+ return err
+ }
+ ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
+ res, err := s3client.ListObjectsV2(ctx, &s3.ListObjectsV2Input{
+ Bucket: &bucket,
+ })
+ cancel()
+ if err != nil {
+ return err
+ }
+
+ // it's en expected bahvior in posix to normalize the object pahts,
+ // by removing multiple slashes
+ normalizedObjs := []string{
+ "bar",
+ "foo/bar",
+ "foo/baz/bar",
+ "foo/quxx",
+ "obj",
+ }
+
+ for i := range objs {
+ objs[i].Key = &normalizedObjs[i]
+ }
+
+ if !compareObjects(objs, res.Contents) {
+ return fmt.Errorf("expected the objects to be %vß, instead got %v",
+ objStrings(objs), objStrings(res.Contents))
+ }
+
+ return nil
+ })
+}
+
func CreateMultipartUpload_dir_obj(s *S3Conf) error {
testName := "CreateMultipartUpload_dir_obj"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
@@ -20879,3 +20868,96 @@ func Versioning_concurrent_upload_object(s *S3Conf) error {
return nil
}, withVersioning(types.BucketVersioningStatusEnabled))
}
+
+// router tests
+func RouterPutPartNumberWithoutUploadId(s *S3Conf) error {
+ testName := "RouterPutPartNumberWithoutUploadId"
+ return actionHandlerNoSetup(s, testName, func(s3client *s3.Client, bucket string) error {
+ req, err := http.NewRequest(http.MethodPut, s.endpoint+"/bucket/object", nil)
+ if err != nil {
+ return err
+ }
+
+ query := req.URL.Query()
+ query.Add("partNumber", "1")
+ req.URL.RawQuery = query.Encode()
+
+ resp, err := s.httpClient.Do(req)
+ if err != nil {
+ return err
+ }
+
+ if err := checkHTTPResponseApiErr(resp, s3err.GetAPIError(s3err.ErrMissingUploadId)); err != nil {
+ return err
+ }
+
+ return nil
+ })
+}
+
+func RouterPostRoot(s *S3Conf) error {
+ testName := "RouterPostRoot"
+ return actionHandlerNoSetup(s, testName, func(s3client *s3.Client, bucket string) error {
+ req, err := http.NewRequest(http.MethodPost, s.endpoint+"/", nil)
+ if err != nil {
+ return err
+ }
+
+ resp, err := s.httpClient.Do(req)
+ if err != nil {
+ return err
+ }
+
+ if err := checkHTTPResponseApiErr(resp, s3err.GetAPIError(s3err.ErrMethodNotAllowed)); err != nil {
+ return err
+ }
+
+ return nil
+ })
+}
+
+func RouterPostObjectWithoutQuery(s *S3Conf) error {
+ testName := "RouterPostObjectWithoutQuery"
+ return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
+ req, err := http.NewRequest(http.MethodPost, s.endpoint+"/bucket/object", nil)
+ if err != nil {
+ return err
+ }
+
+ resp, err := s.httpClient.Do(req)
+ if err != nil {
+ return err
+ }
+
+ if err := checkHTTPResponseApiErr(resp, s3err.GetAPIError(s3err.ErrMethodNotAllowed)); err != nil {
+ return err
+ }
+
+ return nil
+ })
+}
+
+func RouterPUTObjectOnlyUploadId(s *S3Conf) error {
+ testName := "RouterPUTObjectOnlyUploadId"
+ return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
+ req, err := http.NewRequest(http.MethodPut, s.endpoint+"/bucket/object", nil)
+ if err != nil {
+ return err
+ }
+
+ query := req.URL.Query()
+ query.Add("uploadId", "my-upload-id")
+ req.URL.RawQuery = query.Encode()
+
+ resp, err := s.httpClient.Do(req)
+ if err != nil {
+ return err
+ }
+
+ if err := checkHTTPResponseApiErr(resp, s3err.GetAPIError(s3err.ErrMethodNotAllowed)); err != nil {
+ return err
+ }
+
+ return nil
+ })
+}
diff --git a/tests/integration/utils.go b/tests/integration/utils.go
index 569724db..1ff2a1aa 100644
--- a/tests/integration/utils.go
+++ b/tests/integration/utils.go
@@ -284,11 +284,23 @@ func authHandler(s *S3Conf, cfg *authConfig, handler func(req *http.Request) err
return nil
}
-func presignedAuthHandler(s *S3Conf, testName string, handler func(client *s3.PresignClient) error) error {
+func presignedAuthHandler(s *S3Conf, testName string, handler func(client *s3.PresignClient, bucket string) error) error {
runF(testName)
- clt := s3.NewPresignClient(s.GetClient())
+ bucket := getBucketName()
+ err := setup(s, bucket)
+ if err != nil {
+ failF("%v: %v", testName, err)
+ return fmt.Errorf("%v: %w", testName, err)
+ }
+ clt := s.GetPresignClient()
- err := handler(clt)
+ err = handler(clt, bucket)
+ if err != nil {
+ failF("%v: %v", testName, err)
+ return fmt.Errorf("%v: %w", testName, err)
+ }
+
+ err = teardown(s, bucket)
if err != nil {
failF("%v: %v", testName, err)
return fmt.Errorf("%v: %w", testName, err)
@@ -322,7 +334,7 @@ func createSignedReq(method, endpoint, path, access, secret, service, region str
return req, nil
}
-func checkAuthErr(resp *http.Response, apiErr s3err.APIError) error {
+func checkHTTPResponseApiErr(resp *http.Response, apiErr s3err.APIError) error {
body, err := io.ReadAll(resp.Body)
if err != nil {
return err