mirror of
https://github.com/versity/versitygw.git
synced 2026-05-12 15:01:27 +00:00
feat: add option for default global cors allow origin headers
There is some desire to have a web dashboard for the gateway. So that we dont have to proxy all requests through the webserver and expose credentials over the wire, the better approach would be to enable CORS headers to allow browser requests directly to the s3/admin service. The default for these headers is off, so that they are only enabled for instances that specfically want to support this workload.
This commit is contained in:
@@ -42,6 +42,7 @@ var (
|
||||
rootUserAccess string
|
||||
rootUserSecret string
|
||||
region string
|
||||
corsAllowOrigin string
|
||||
admCertFile, admKeyFile string
|
||||
certFile, keyFile string
|
||||
kafkaURL, kafkaTopic, kafkaKey string
|
||||
@@ -188,6 +189,12 @@ func initFlags() []cli.Flag {
|
||||
Destination: ®ion,
|
||||
Aliases: []string{"r"},
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "cors-allow-origin",
|
||||
Usage: "default CORS Access-Control-Allow-Origin value (applied when no bucket CORS configuration exists, and for admin APIs)",
|
||||
EnvVars: []string{"VGW_CORS_ALLOW_ORIGIN"},
|
||||
Destination: &corsAllowOrigin,
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "cert",
|
||||
Usage: "TLS cert file",
|
||||
@@ -649,6 +656,9 @@ func runGateway(ctx context.Context, be backend.Backend) error {
|
||||
}
|
||||
|
||||
var opts []s3api.Option
|
||||
if corsAllowOrigin != "" {
|
||||
opts = append(opts, s3api.WithCORSAllowOrigin(corsAllowOrigin))
|
||||
}
|
||||
|
||||
if certFile != "" || keyFile != "" {
|
||||
if certFile == "" {
|
||||
@@ -786,6 +796,9 @@ func runGateway(ctx context.Context, be backend.Backend) error {
|
||||
|
||||
if admPort != "" {
|
||||
var opts []s3api.AdminOpt
|
||||
if corsAllowOrigin != "" {
|
||||
opts = append(opts, s3api.WithAdminCORSAllowOrigin(corsAllowOrigin))
|
||||
}
|
||||
|
||||
if admCertFile != "" || admKeyFile != "" {
|
||||
if admCertFile == "" {
|
||||
|
||||
@@ -26,7 +26,7 @@ import (
|
||||
|
||||
type S3AdminRouter struct{}
|
||||
|
||||
func (ar *S3AdminRouter) Init(app *fiber.App, be backend.Backend, iam auth.IAMService, logger s3log.AuditLogger, root middlewares.RootUserConfig, region string, debug bool) {
|
||||
func (ar *S3AdminRouter) Init(app *fiber.App, be backend.Backend, iam auth.IAMService, logger s3log.AuditLogger, root middlewares.RootUserConfig, region string, debug bool, corsAllowOrigin string) {
|
||||
ctrl := controllers.NewAdminController(iam, be, logger)
|
||||
services := &controllers.Services{
|
||||
Logger: logger,
|
||||
@@ -37,40 +37,70 @@ func (ar *S3AdminRouter) Init(app *fiber.App, be backend.Backend, iam auth.IAMSe
|
||||
controllers.ProcessHandlers(ctrl.CreateUser, metrics.ActionAdminCreateUser, services,
|
||||
middlewares.VerifyV4Signature(root, iam, region, false, true),
|
||||
middlewares.IsAdmin(metrics.ActionAdminCreateUser),
|
||||
middlewares.ApplyDefaultCORS(corsAllowOrigin),
|
||||
))
|
||||
app.Options("/create-user",
|
||||
middlewares.ApplyDefaultCORSPreflight(corsAllowOrigin),
|
||||
middlewares.ApplyDefaultCORS(corsAllowOrigin),
|
||||
)
|
||||
|
||||
// DeleteUsers admin api
|
||||
app.Patch("/delete-user",
|
||||
controllers.ProcessHandlers(ctrl.DeleteUser, metrics.ActionAdminDeleteUser, services,
|
||||
middlewares.VerifyV4Signature(root, iam, region, false, true),
|
||||
middlewares.IsAdmin(metrics.ActionAdminDeleteUser),
|
||||
middlewares.ApplyDefaultCORS(corsAllowOrigin),
|
||||
))
|
||||
app.Options("/delete-user",
|
||||
middlewares.ApplyDefaultCORSPreflight(corsAllowOrigin),
|
||||
middlewares.ApplyDefaultCORS(corsAllowOrigin),
|
||||
)
|
||||
|
||||
// UpdateUser admin api
|
||||
app.Patch("/update-user",
|
||||
controllers.ProcessHandlers(ctrl.UpdateUser, metrics.ActionAdminUpdateUser, services,
|
||||
middlewares.VerifyV4Signature(root, iam, region, false, true),
|
||||
middlewares.IsAdmin(metrics.ActionAdminUpdateUser),
|
||||
middlewares.ApplyDefaultCORS(corsAllowOrigin),
|
||||
))
|
||||
app.Options("/update-user",
|
||||
middlewares.ApplyDefaultCORSPreflight(corsAllowOrigin),
|
||||
middlewares.ApplyDefaultCORS(corsAllowOrigin),
|
||||
)
|
||||
|
||||
// ListUsers admin api
|
||||
app.Patch("/list-users",
|
||||
controllers.ProcessHandlers(ctrl.ListUsers, metrics.ActionAdminListUsers, services,
|
||||
middlewares.VerifyV4Signature(root, iam, region, false, true),
|
||||
middlewares.IsAdmin(metrics.ActionAdminListUsers),
|
||||
middlewares.ApplyDefaultCORS(corsAllowOrigin),
|
||||
))
|
||||
app.Options("/list-users",
|
||||
middlewares.ApplyDefaultCORSPreflight(corsAllowOrigin),
|
||||
middlewares.ApplyDefaultCORS(corsAllowOrigin),
|
||||
)
|
||||
|
||||
// ChangeBucketOwner admin api
|
||||
app.Patch("/change-bucket-owner",
|
||||
controllers.ProcessHandlers(ctrl.ChangeBucketOwner, metrics.ActionAdminChangeBucketOwner, services,
|
||||
middlewares.VerifyV4Signature(root, iam, region, false, true),
|
||||
middlewares.IsAdmin(metrics.ActionAdminChangeBucketOwner),
|
||||
middlewares.ApplyDefaultCORS(corsAllowOrigin),
|
||||
))
|
||||
app.Options("/change-bucket-owner",
|
||||
middlewares.ApplyDefaultCORSPreflight(corsAllowOrigin),
|
||||
middlewares.ApplyDefaultCORS(corsAllowOrigin),
|
||||
)
|
||||
|
||||
// ListBucketsAndOwners admin api
|
||||
app.Patch("/list-buckets",
|
||||
controllers.ProcessHandlers(ctrl.ListBuckets, metrics.ActionAdminListBuckets, services,
|
||||
middlewares.VerifyV4Signature(root, iam, region, false, true),
|
||||
middlewares.IsAdmin(metrics.ActionAdminListBuckets),
|
||||
middlewares.ApplyDefaultCORS(corsAllowOrigin),
|
||||
))
|
||||
app.Options("/list-buckets",
|
||||
middlewares.ApplyDefaultCORSPreflight(corsAllowOrigin),
|
||||
middlewares.ApplyDefaultCORS(corsAllowOrigin),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -28,13 +28,14 @@ import (
|
||||
)
|
||||
|
||||
type S3AdminServer struct {
|
||||
app *fiber.App
|
||||
backend backend.Backend
|
||||
router *S3AdminRouter
|
||||
port string
|
||||
cert *tls.Certificate
|
||||
quiet bool
|
||||
debug bool
|
||||
app *fiber.App
|
||||
backend backend.Backend
|
||||
router *S3AdminRouter
|
||||
port string
|
||||
cert *tls.Certificate
|
||||
quiet bool
|
||||
debug bool
|
||||
corsAllowOrigin string
|
||||
}
|
||||
|
||||
func NewAdminServer(be backend.Backend, root middlewares.RootUserConfig, port, region string, iam auth.IAMService, l s3log.AuditLogger, opts ...AdminOpt) *S3AdminServer {
|
||||
@@ -73,7 +74,7 @@ func NewAdminServer(be backend.Backend, root middlewares.RootUserConfig, port, r
|
||||
app.Use(controllers.WrapMiddleware(middlewares.DecodeURL, l, nil))
|
||||
app.Use(middlewares.DebugLogger())
|
||||
|
||||
server.router.Init(app, be, iam, l, root, region, server.debug)
|
||||
server.router.Init(app, be, iam, l, root, region, server.debug, server.corsAllowOrigin)
|
||||
|
||||
return server
|
||||
}
|
||||
@@ -94,6 +95,12 @@ func WithAdminDebug() AdminOpt {
|
||||
return func(s *S3AdminServer) { s.debug = true }
|
||||
}
|
||||
|
||||
// WithAdminCORSAllowOrigin sets the default CORS Access-Control-Allow-Origin value
|
||||
// for the standalone admin server.
|
||||
func WithAdminCORSAllowOrigin(origin string) AdminOpt {
|
||||
return func(s *S3AdminServer) { s.corsAllowOrigin = origin }
|
||||
}
|
||||
|
||||
func (sa *S3AdminServer) Serve() (err error) {
|
||||
if sa.cert != nil {
|
||||
return sa.app.ListenTLSWithCertificate(sa.port, *sa.cert)
|
||||
|
||||
@@ -18,6 +18,8 @@ import (
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/versity/versitygw/auth"
|
||||
@@ -172,6 +174,7 @@ func ProcessController(ctx *fiber.Ctx, controller Controller, s3action string, s
|
||||
|
||||
// Set the response headers
|
||||
SetResponseHeaders(ctx, response.Headers)
|
||||
ensureExposeMetaHeaders(ctx)
|
||||
|
||||
opts := response.MetaOpts
|
||||
if opts == nil {
|
||||
@@ -314,6 +317,77 @@ func ProcessController(ctx *fiber.Ctx, controller Controller, s3action string, s
|
||||
return ctx.Send(res)
|
||||
}
|
||||
|
||||
func ensureExposeMetaHeaders(ctx *fiber.Ctx) {
|
||||
// Only attempt to modify expose headers when CORS is actually in use.
|
||||
if len(ctx.Response().Header.Peek("Access-Control-Allow-Origin")) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
existing := strings.TrimSpace(string(ctx.Response().Header.Peek("Access-Control-Expose-Headers")))
|
||||
if existing == "*" {
|
||||
return
|
||||
}
|
||||
|
||||
lowerExisting := map[string]struct{}{}
|
||||
if existing != "" {
|
||||
for _, part := range strings.Split(existing, ",") {
|
||||
p := strings.ToLower(strings.TrimSpace(part))
|
||||
if p != "" {
|
||||
lowerExisting[p] = struct{}{}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
metaNames := map[string]struct{}{}
|
||||
for k := range ctx.Response().Header.All() {
|
||||
key := string(k)
|
||||
if strings.HasPrefix(strings.ToLower(key), "x-amz-meta-") {
|
||||
metaNames[key] = struct{}{}
|
||||
}
|
||||
}
|
||||
if len(metaNames) == 0 {
|
||||
// Still ensure ETag is present if any expose headers exist/are needed.
|
||||
if _, ok := lowerExisting["etag"]; ok {
|
||||
return
|
||||
}
|
||||
if existing == "" {
|
||||
ctx.Response().Header.Set("Access-Control-Expose-Headers", "ETag")
|
||||
return
|
||||
}
|
||||
ctx.Response().Header.Set("Access-Control-Expose-Headers", existing+", ETag")
|
||||
return
|
||||
}
|
||||
|
||||
metaList := make([]string, 0, len(metaNames))
|
||||
for k := range metaNames {
|
||||
metaList = append(metaList, k)
|
||||
}
|
||||
sort.Strings(metaList)
|
||||
|
||||
toAdd := make([]string, 0, 1+len(metaList))
|
||||
if _, ok := lowerExisting["etag"]; !ok {
|
||||
toAdd = append(toAdd, "ETag")
|
||||
lowerExisting["etag"] = struct{}{}
|
||||
}
|
||||
for _, h := range metaList {
|
||||
lh := strings.ToLower(h)
|
||||
if _, ok := lowerExisting[lh]; ok {
|
||||
continue
|
||||
}
|
||||
toAdd = append(toAdd, h)
|
||||
lowerExisting[lh] = struct{}{}
|
||||
}
|
||||
if len(toAdd) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
if existing == "" {
|
||||
ctx.Response().Header.Set("Access-Control-Expose-Headers", strings.Join(toAdd, ", "))
|
||||
return
|
||||
}
|
||||
ctx.Response().Header.Set("Access-Control-Expose-Headers", existing+", "+strings.Join(toAdd, ", "))
|
||||
}
|
||||
|
||||
// Sets the response headers
|
||||
func SetResponseHeaders(ctx *fiber.Ctx, headers map[string]*string) {
|
||||
if headers == nil {
|
||||
|
||||
@@ -237,6 +237,21 @@ func TestSetResponseHeaders(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnsureExposeMetaHeaders_AddsActualMetaHeaderNames(t *testing.T) {
|
||||
app := fiber.New()
|
||||
ctx := app.AcquireCtx(&fasthttp.RequestCtx{})
|
||||
|
||||
ctx.Response().Header.Add("Access-Control-Allow-Origin", "https://example.com")
|
||||
ctx.Response().Header.Add("Access-Control-Expose-Headers", "ETag")
|
||||
ctx.Response().Header.Set("x-amz-meta-foo", "bar")
|
||||
ctx.Response().Header.Set("x-amz-meta-bar", "baz")
|
||||
|
||||
ensureExposeMetaHeaders(ctx)
|
||||
|
||||
got := string(ctx.Response().Header.Peek("Access-Control-Expose-Headers"))
|
||||
assert.Equal(t, "ETag, X-Amz-Meta-Bar, X-Amz-Meta-Foo", got)
|
||||
}
|
||||
|
||||
// mock the audit logger
|
||||
type mockAuditLogger struct {
|
||||
}
|
||||
|
||||
92
s3api/controllers/cors_default_origin_test.go
Normal file
92
s3api/controllers/cors_default_origin_test.go
Normal file
@@ -0,0 +1,92 @@
|
||||
// Copyright 2026 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/gofiber/fiber/v2"
|
||||
"github.com/versity/versitygw/s3api/middlewares"
|
||||
"github.com/versity/versitygw/s3err"
|
||||
)
|
||||
|
||||
func TestApplyBucketCORS_FallbackOrigin_NoBucketCors_NoRequestOrigin(t *testing.T) {
|
||||
origin := "https://example.com"
|
||||
|
||||
mockedBackend := &BackendMock{
|
||||
GetBucketCorsFunc: func(ctx context.Context, bucket string) ([]byte, error) {
|
||||
return nil, s3err.GetAPIError(s3err.ErrNoSuchCORSConfiguration)
|
||||
},
|
||||
}
|
||||
|
||||
app := fiber.New()
|
||||
app.Get("/:bucket/test",
|
||||
middlewares.ApplyBucketCORS(mockedBackend, origin),
|
||||
func(c *fiber.Ctx) error {
|
||||
return c.SendStatus(http.StatusOK)
|
||||
},
|
||||
)
|
||||
|
||||
req, err := http.NewRequest(http.MethodGet, "/mybucket/test", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("new request: %v", err)
|
||||
}
|
||||
|
||||
resp, err := app.Test(req)
|
||||
if err != nil {
|
||||
t.Fatalf("app.Test: %v", err)
|
||||
}
|
||||
|
||||
if got := resp.Header.Get("Access-Control-Allow-Origin"); got != origin {
|
||||
t.Fatalf("expected Access-Control-Allow-Origin to be set to fallback, got %q", got)
|
||||
}
|
||||
if got := resp.Header.Get("Access-Control-Expose-Headers"); got != "ETag" {
|
||||
t.Fatalf("expected Access-Control-Expose-Headers to include ETag, got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyBucketCORS_FallbackOrigin_NotAppliedWhenBucketCorsExists(t *testing.T) {
|
||||
origin := "https://example.com"
|
||||
|
||||
mockedBackend := &BackendMock{
|
||||
GetBucketCorsFunc: func(ctx context.Context, bucket string) ([]byte, error) {
|
||||
return []byte("not-parsed"), nil
|
||||
},
|
||||
}
|
||||
|
||||
app := fiber.New()
|
||||
app.Get("/:bucket/test",
|
||||
middlewares.ApplyBucketCORS(mockedBackend, origin),
|
||||
func(c *fiber.Ctx) error {
|
||||
return c.SendStatus(http.StatusOK)
|
||||
},
|
||||
)
|
||||
|
||||
req, err := http.NewRequest(http.MethodGet, "/mybucket/test", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("new request: %v", err)
|
||||
}
|
||||
|
||||
resp, err := app.Test(req)
|
||||
if err != nil {
|
||||
t.Fatalf("app.Test: %v", err)
|
||||
}
|
||||
|
||||
if got := resp.Header.Get("Access-Control-Allow-Origin"); got != "" {
|
||||
t.Fatalf("expected no Access-Control-Allow-Origin when bucket CORS exists, got %q", got)
|
||||
}
|
||||
}
|
||||
73
s3api/middlewares/apply-bucket-cors-preflight.go
Normal file
73
s3api/middlewares/apply-bucket-cors-preflight.go
Normal file
@@ -0,0 +1,73 @@
|
||||
// Copyright 2026 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 (
|
||||
"strings"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/versity/versitygw/backend"
|
||||
"github.com/versity/versitygw/s3err"
|
||||
)
|
||||
|
||||
// ApplyBucketCORSPreflightFallback handles CORS preflight (OPTIONS) requests for S3 routes
|
||||
// when no per-bucket CORS configuration exists.
|
||||
//
|
||||
// If the bucket has no CORS configuration and fallbackOrigin is set, it responds with 204 and:
|
||||
// - Access-Control-Allow-Origin: fallbackOrigin
|
||||
// - Vary: Origin, Access-Control-Request-Headers, Access-Control-Request-Method
|
||||
// - Access-Control-Allow-Methods: mirrors Access-Control-Request-Method (if present)
|
||||
// - Access-Control-Allow-Headers: mirrors Access-Control-Request-Headers (if present)
|
||||
//
|
||||
// If the bucket has a CORS configuration (or fallbackOrigin is blank), it calls next so the
|
||||
// standard CORS OPTIONS handler can apply bucket-specific rules.
|
||||
func ApplyBucketCORSPreflightFallback(be backend.Backend, fallbackOrigin string) fiber.Handler {
|
||||
fallbackOrigin = strings.TrimSpace(fallbackOrigin)
|
||||
if fallbackOrigin == "" {
|
||||
return func(ctx *fiber.Ctx) error { return ctx.Next() }
|
||||
}
|
||||
|
||||
return func(ctx *fiber.Ctx) error {
|
||||
bucket := ctx.Params("bucket")
|
||||
_, err := be.GetBucketCors(ctx.Context(), bucket)
|
||||
if err != nil {
|
||||
if s3Err, ok := err.(s3err.APIError); ok && (s3Err.Code == "NoSuchCORSConfiguration" || s3Err.Code == "NoSuchBucket") {
|
||||
if len(ctx.Response().Header.Peek("Access-Control-Allow-Origin")) == 0 {
|
||||
ctx.Response().Header.Add("Access-Control-Allow-Origin", fallbackOrigin)
|
||||
}
|
||||
if len(ctx.Response().Header.Peek("Vary")) == 0 {
|
||||
ctx.Response().Header.Add("Vary", VaryHdr)
|
||||
}
|
||||
|
||||
if reqMethod := strings.TrimSpace(ctx.Get("Access-Control-Request-Method")); reqMethod != "" {
|
||||
if len(ctx.Response().Header.Peek("Access-Control-Allow-Methods")) == 0 {
|
||||
ctx.Response().Header.Add("Access-Control-Allow-Methods", reqMethod)
|
||||
}
|
||||
}
|
||||
|
||||
if reqHeaders := strings.TrimSpace(ctx.Get("Access-Control-Request-Headers")); reqHeaders != "" {
|
||||
if len(ctx.Response().Header.Peek("Access-Control-Allow-Headers")) == 0 {
|
||||
ctx.Response().Header.Add("Access-Control-Allow-Headers", reqHeaders)
|
||||
}
|
||||
}
|
||||
|
||||
ctx.Status(fiber.StatusNoContent)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return ctx.Next()
|
||||
}
|
||||
}
|
||||
146
s3api/middlewares/apply-bucket-cors-preflight_test.go
Normal file
146
s3api/middlewares/apply-bucket-cors-preflight_test.go
Normal file
@@ -0,0 +1,146 @@
|
||||
// Copyright 2026 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 (
|
||||
"context"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/versity/versitygw/backend"
|
||||
"github.com/versity/versitygw/s3err"
|
||||
)
|
||||
|
||||
type backendWithGetBucketCors struct {
|
||||
backend.BackendUnsupported
|
||||
getBucketCors func(ctx context.Context, bucket string) ([]byte, error)
|
||||
}
|
||||
|
||||
func (b backendWithGetBucketCors) GetBucketCors(ctx context.Context, bucket string) ([]byte, error) {
|
||||
return b.getBucketCors(ctx, bucket)
|
||||
}
|
||||
|
||||
func TestApplyBucketCORSPreflightFallback_NoBucketCors_Responds204(t *testing.T) {
|
||||
be := backendWithGetBucketCors{
|
||||
getBucketCors: func(ctx context.Context, bucket string) ([]byte, error) {
|
||||
return nil, s3err.GetAPIError(s3err.ErrNoSuchCORSConfiguration)
|
||||
},
|
||||
}
|
||||
|
||||
app := fiber.New()
|
||||
app.Options("/:bucket",
|
||||
ApplyBucketCORSPreflightFallback(be, "https://example.com"),
|
||||
func(c *fiber.Ctx) error {
|
||||
// Should not be reached if fallback triggers
|
||||
return c.SendStatus(http.StatusTeapot)
|
||||
},
|
||||
)
|
||||
|
||||
req, err := http.NewRequest(http.MethodOptions, "/testing", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("new request: %v", err)
|
||||
}
|
||||
req.Header.Set("Origin", "https://request-origin.example")
|
||||
req.Header.Set("Access-Control-Request-Method", "GET")
|
||||
req.Header.Set("Access-Control-Request-Headers", "content-type")
|
||||
|
||||
resp, err := app.Test(req)
|
||||
if err != nil {
|
||||
t.Fatalf("app.Test: %v", err)
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusNoContent {
|
||||
t.Fatalf("expected status 204, got %d", resp.StatusCode)
|
||||
}
|
||||
if got := resp.Header.Get("Access-Control-Allow-Origin"); got != "https://example.com" {
|
||||
t.Fatalf("expected allow origin fallback, got %q", got)
|
||||
}
|
||||
if got := resp.Header.Get("Access-Control-Allow-Methods"); got != "GET" {
|
||||
t.Fatalf("expected allow methods to mirror request, got %q", got)
|
||||
}
|
||||
if got := resp.Header.Get("Access-Control-Allow-Headers"); got != "content-type" {
|
||||
t.Fatalf("expected allow headers to mirror request, got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyBucketCORSPreflightFallback_NoSuchBucket_Responds204(t *testing.T) {
|
||||
be := backendWithGetBucketCors{
|
||||
getBucketCors: func(ctx context.Context, bucket string) ([]byte, error) {
|
||||
return nil, s3err.GetAPIError(s3err.ErrNoSuchBucket)
|
||||
},
|
||||
}
|
||||
|
||||
app := fiber.New()
|
||||
app.Options("/:bucket",
|
||||
ApplyBucketCORSPreflightFallback(be, "https://example.com"),
|
||||
func(c *fiber.Ctx) error {
|
||||
return c.SendStatus(http.StatusTeapot)
|
||||
},
|
||||
)
|
||||
|
||||
req, err := http.NewRequest(http.MethodOptions, "/testing", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("new request: %v", err)
|
||||
}
|
||||
req.Header.Set("Origin", "https://request-origin.example")
|
||||
req.Header.Set("Access-Control-Request-Method", "PUT")
|
||||
req.Header.Set("Access-Control-Request-Headers", "content-type")
|
||||
|
||||
resp, err := app.Test(req)
|
||||
if err != nil {
|
||||
t.Fatalf("app.Test: %v", err)
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusNoContent {
|
||||
t.Fatalf("expected status 204, got %d", resp.StatusCode)
|
||||
}
|
||||
if got := resp.Header.Get("Access-Control-Allow-Origin"); got != "https://example.com" {
|
||||
t.Fatalf("expected allow origin fallback, got %q", got)
|
||||
}
|
||||
if got := resp.Header.Get("Access-Control-Allow-Methods"); got != "PUT" {
|
||||
t.Fatalf("expected allow methods to mirror request, got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyBucketCORSPreflightFallback_BucketHasCors_CallsNext(t *testing.T) {
|
||||
be := backendWithGetBucketCors{
|
||||
getBucketCors: func(ctx context.Context, bucket string) ([]byte, error) {
|
||||
return []byte("dummy"), nil
|
||||
},
|
||||
}
|
||||
|
||||
app := fiber.New()
|
||||
app.Options("/:bucket",
|
||||
ApplyBucketCORSPreflightFallback(be, "https://example.com"),
|
||||
func(c *fiber.Ctx) error {
|
||||
return c.SendStatus(http.StatusOK)
|
||||
},
|
||||
)
|
||||
|
||||
req, err := http.NewRequest(http.MethodOptions, "/testing", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("new request: %v", err)
|
||||
}
|
||||
|
||||
resp, err := app.Test(req)
|
||||
if err != nil {
|
||||
t.Fatalf("app.Test: %v", err)
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Fatalf("expected status 200 from next handler, got %d", resp.StatusCode)
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,7 @@ package middlewares
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/versity/versitygw/auth"
|
||||
@@ -31,12 +32,14 @@ var VaryHdr = "Origin, Access-Control-Request-Headers, Access-Control-Request-Me
|
||||
// checks if origin and method meets the cors rules and
|
||||
// adds the necessary response headers.
|
||||
// CORS check is applied only when 'Origin' request header is present
|
||||
func ApplyBucketCORS(be backend.Backend) fiber.Handler {
|
||||
func ApplyBucketCORS(be backend.Backend, fallbackOrigin string) fiber.Handler {
|
||||
fallbackOrigin = strings.TrimSpace(fallbackOrigin)
|
||||
|
||||
return func(ctx *fiber.Ctx) error {
|
||||
bucket := ctx.Params("bucket")
|
||||
origin := ctx.Get("Origin")
|
||||
// if the origin request header is empty, skip cors validation
|
||||
if origin == "" {
|
||||
// If neither Origin is present nor a fallback is configured, skip CORS entirely.
|
||||
if origin == "" && fallbackOrigin == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -46,12 +49,32 @@ func ApplyBucketCORS(be backend.Backend) fiber.Handler {
|
||||
// If CORS is not configured, S3Error will have code NoSuchCORSConfiguration.
|
||||
// In this case, we can safely continue. For any other error, we should log it.
|
||||
s3Err, ok := err.(s3err.APIError)
|
||||
if ok && (s3Err.Code == "NoSuchCORSConfiguration" || s3Err.Code == "NoSuchBucket") {
|
||||
// Optional global fallback: add Access-Control-Allow-Origin for buckets
|
||||
// without a specific CORS configuration.
|
||||
if fallbackOrigin != "" {
|
||||
if len(ctx.Response().Header.Peek("Access-Control-Allow-Origin")) == 0 {
|
||||
ctx.Response().Header.Add("Access-Control-Allow-Origin", fallbackOrigin)
|
||||
}
|
||||
if len(ctx.Response().Header.Peek("Vary")) == 0 {
|
||||
ctx.Response().Header.Add("Vary", VaryHdr)
|
||||
}
|
||||
ensureExposeETag(ctx)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if !ok || s3Err.Code != "NoSuchCORSConfiguration" {
|
||||
debuglogger.Logf("failed to get bucket cors for bucket %q: %v", bucket, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// If Origin is missing, don't attempt per-bucket CORS evaluation.
|
||||
// (Fallback has already been handled above for buckets without CORS config.)
|
||||
if origin == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
cors, err := auth.ParseCORSOutput(data)
|
||||
if err != nil {
|
||||
return nil
|
||||
@@ -100,6 +123,9 @@ func ApplyBucketCORS(be backend.Backend) fiber.Handler {
|
||||
}
|
||||
}
|
||||
|
||||
// Always expose ETag and user metadata headers for browser clients.
|
||||
ensureExposeETag(ctx)
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
58
s3api/middlewares/apply-default-cors-preflight.go
Normal file
58
s3api/middlewares/apply-default-cors-preflight.go
Normal file
@@ -0,0 +1,58 @@
|
||||
// Copyright 2026 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 (
|
||||
"strings"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
// ApplyDefaultCORSPreflight responds to CORS preflight (OPTIONS) requests for routes
|
||||
// that don't have per-bucket CORS configuration (e.g. admin APIs).
|
||||
//
|
||||
// It uses the provided fallbackOrigin as the Access-Control-Allow-Origin value.
|
||||
// It mirrors Access-Control-Request-Method into Access-Control-Allow-Methods and
|
||||
// mirrors Access-Control-Request-Headers into Access-Control-Allow-Headers.
|
||||
func ApplyDefaultCORSPreflight(fallbackOrigin string) fiber.Handler {
|
||||
fallbackOrigin = strings.TrimSpace(fallbackOrigin)
|
||||
if fallbackOrigin == "" {
|
||||
return func(ctx *fiber.Ctx) error { return nil }
|
||||
}
|
||||
|
||||
return func(ctx *fiber.Ctx) error {
|
||||
if len(ctx.Response().Header.Peek("Access-Control-Allow-Origin")) == 0 {
|
||||
ctx.Response().Header.Add("Access-Control-Allow-Origin", fallbackOrigin)
|
||||
}
|
||||
if len(ctx.Response().Header.Peek("Vary")) == 0 {
|
||||
ctx.Response().Header.Add("Vary", VaryHdr)
|
||||
}
|
||||
|
||||
if reqMethod := strings.TrimSpace(ctx.Get("Access-Control-Request-Method")); reqMethod != "" {
|
||||
if len(ctx.Response().Header.Peek("Access-Control-Allow-Methods")) == 0 {
|
||||
ctx.Response().Header.Add("Access-Control-Allow-Methods", reqMethod)
|
||||
}
|
||||
}
|
||||
|
||||
if reqHeaders := strings.TrimSpace(ctx.Get("Access-Control-Request-Headers")); reqHeaders != "" {
|
||||
if len(ctx.Response().Header.Peek("Access-Control-Allow-Headers")) == 0 {
|
||||
ctx.Response().Header.Add("Access-Control-Allow-Headers", reqHeaders)
|
||||
}
|
||||
}
|
||||
|
||||
ctx.Status(fiber.StatusNoContent)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
59
s3api/middlewares/apply-default-cors-preflight_test.go
Normal file
59
s3api/middlewares/apply-default-cors-preflight_test.go
Normal file
@@ -0,0 +1,59 @@
|
||||
// Copyright 2026 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 (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
func TestApplyDefaultCORSPreflight_OptionsSetsPreflightHeaders(t *testing.T) {
|
||||
origin := "https://example.com"
|
||||
|
||||
app := fiber.New()
|
||||
app.Options("/admin",
|
||||
ApplyDefaultCORSPreflight(origin),
|
||||
ApplyDefaultCORS(origin),
|
||||
func(c *fiber.Ctx) error { return nil },
|
||||
)
|
||||
|
||||
req, err := http.NewRequest(http.MethodOptions, "/admin", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("new request: %v", err)
|
||||
}
|
||||
req.Header.Set("Origin", "https://request-origin.example")
|
||||
req.Header.Set("Access-Control-Request-Method", "PATCH")
|
||||
req.Header.Set("Access-Control-Request-Headers", "content-type,authorization")
|
||||
|
||||
resp, err := app.Test(req)
|
||||
if err != nil {
|
||||
t.Fatalf("app.Test: %v", err)
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusNoContent {
|
||||
t.Fatalf("expected status 204, got %d", resp.StatusCode)
|
||||
}
|
||||
if got := resp.Header.Get("Access-Control-Allow-Origin"); got != origin {
|
||||
t.Fatalf("expected allow origin fallback, got %q", got)
|
||||
}
|
||||
if got := resp.Header.Get("Access-Control-Allow-Methods"); got != "PATCH" {
|
||||
t.Fatalf("expected allow methods to mirror request, got %q", got)
|
||||
}
|
||||
if got := resp.Header.Get("Access-Control-Allow-Headers"); got != "content-type,authorization" {
|
||||
t.Fatalf("expected allow headers to mirror request, got %q", got)
|
||||
}
|
||||
}
|
||||
73
s3api/middlewares/apply-default-cors.go
Normal file
73
s3api/middlewares/apply-default-cors.go
Normal file
@@ -0,0 +1,73 @@
|
||||
// Copyright 2026 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 (
|
||||
"strings"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
func ensureExposeETag(ctx *fiber.Ctx) {
|
||||
existing := strings.TrimSpace(string(ctx.Response().Header.Peek("Access-Control-Expose-Headers")))
|
||||
defaults := []string{"ETag"}
|
||||
if existing == "" {
|
||||
ctx.Response().Header.Add("Access-Control-Expose-Headers", strings.Join(defaults, ", "))
|
||||
return
|
||||
}
|
||||
|
||||
lowerExisting := map[string]struct{}{}
|
||||
for _, part := range strings.Split(existing, ",") {
|
||||
p := strings.ToLower(strings.TrimSpace(part))
|
||||
if p != "" {
|
||||
lowerExisting[p] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
updated := existing
|
||||
for _, h := range defaults {
|
||||
if _, ok := lowerExisting[strings.ToLower(h)]; ok {
|
||||
continue
|
||||
}
|
||||
updated += ", " + h
|
||||
}
|
||||
|
||||
if updated != existing {
|
||||
ctx.Response().Header.Set("Access-Control-Expose-Headers", updated)
|
||||
}
|
||||
}
|
||||
|
||||
// ApplyDefaultCORS adds a default Access-Control-Allow-Origin header to responses
|
||||
// when the provided fallbackOrigin is non-empty.
|
||||
//
|
||||
// This is intended for routes that don't have per-bucket CORS configuration (e.g. admin APIs).
|
||||
// It will not override an existing Access-Control-Allow-Origin header.
|
||||
func ApplyDefaultCORS(fallbackOrigin string) fiber.Handler {
|
||||
fallbackOrigin = strings.TrimSpace(fallbackOrigin)
|
||||
if fallbackOrigin == "" {
|
||||
return func(ctx *fiber.Ctx) error { return nil }
|
||||
}
|
||||
|
||||
return func(ctx *fiber.Ctx) error {
|
||||
if len(ctx.Response().Header.Peek("Access-Control-Allow-Origin")) == 0 {
|
||||
ctx.Response().Header.Add("Access-Control-Allow-Origin", fallbackOrigin)
|
||||
}
|
||||
if len(ctx.Response().Header.Peek("Vary")) == 0 {
|
||||
ctx.Response().Header.Add("Vary", VaryHdr)
|
||||
}
|
||||
ensureExposeETag(ctx)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
74
s3api/middlewares/apply-default-cors_test.go
Normal file
74
s3api/middlewares/apply-default-cors_test.go
Normal file
@@ -0,0 +1,74 @@
|
||||
// Copyright 2026 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 (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
func TestApplyDefaultCORS_AddsHeaderWhenOriginSet(t *testing.T) {
|
||||
origin := "https://example.com"
|
||||
|
||||
app := fiber.New()
|
||||
app.Get("/admin", ApplyDefaultCORS(origin), func(c *fiber.Ctx) error {
|
||||
return c.SendStatus(http.StatusOK)
|
||||
})
|
||||
|
||||
req, err := http.NewRequest(http.MethodGet, "/admin", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("new request: %v", err)
|
||||
}
|
||||
|
||||
resp, err := app.Test(req)
|
||||
if err != nil {
|
||||
t.Fatalf("app.Test: %v", err)
|
||||
}
|
||||
|
||||
if got := resp.Header.Get("Access-Control-Allow-Origin"); got != origin {
|
||||
t.Fatalf("expected fallback origin header, got %q", got)
|
||||
}
|
||||
if got := resp.Header.Get("Access-Control-Expose-Headers"); got != "ETag" {
|
||||
t.Fatalf("expected expose headers to include ETag, got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyDefaultCORS_DoesNotOverrideExistingHeader(t *testing.T) {
|
||||
origin := "https://example.com"
|
||||
|
||||
app := fiber.New()
|
||||
app.Get("/admin", func(c *fiber.Ctx) error {
|
||||
c.Response().Header.Add("Access-Control-Allow-Origin", "https://already-set.com")
|
||||
return nil
|
||||
}, ApplyDefaultCORS(origin), func(c *fiber.Ctx) error {
|
||||
return c.SendStatus(http.StatusOK)
|
||||
})
|
||||
|
||||
req, err := http.NewRequest(http.MethodGet, "/admin", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("new request: %v", err)
|
||||
}
|
||||
|
||||
resp, err := app.Test(req)
|
||||
if err != nil {
|
||||
t.Fatalf("app.Test: %v", err)
|
||||
}
|
||||
|
||||
if got := resp.Header.Get("Access-Control-Allow-Origin"); got != "https://already-set.com" {
|
||||
t.Fatalf("expected existing header to remain, got %q", got)
|
||||
}
|
||||
}
|
||||
169
s3api/router.go
169
s3api/router.go
@@ -30,7 +30,7 @@ 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, readonly bool, region, virtualDomain string, root middlewares.RootUserConfig) {
|
||||
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, readonly bool, region, virtualDomain string, root middlewares.RootUserConfig, corsAllowOrigin string) {
|
||||
ctrl := controllers.New(be, iam, logger, evs, mm, readonly, virtualDomain)
|
||||
adminServices := &controllers.Services{
|
||||
Logger: aLogger,
|
||||
@@ -44,42 +44,72 @@ func (sa *S3ApiRouter) Init(app *fiber.App, be backend.Backend, iam auth.IAMServ
|
||||
controllers.ProcessHandlers(adminController.CreateUser, metrics.ActionAdminCreateUser, adminServices,
|
||||
middlewares.VerifyV4Signature(root, iam, region, false, true),
|
||||
middlewares.IsAdmin(metrics.ActionAdminCreateUser),
|
||||
middlewares.ApplyDefaultCORS(corsAllowOrigin),
|
||||
))
|
||||
app.Options("/create-user",
|
||||
middlewares.ApplyDefaultCORSPreflight(corsAllowOrigin),
|
||||
middlewares.ApplyDefaultCORS(corsAllowOrigin),
|
||||
)
|
||||
|
||||
// DeleteUsers admin api
|
||||
app.Patch("/delete-user",
|
||||
controllers.ProcessHandlers(adminController.DeleteUser, metrics.ActionAdminDeleteUser, adminServices,
|
||||
middlewares.VerifyV4Signature(root, iam, region, false, true),
|
||||
middlewares.IsAdmin(metrics.ActionAdminDeleteUser),
|
||||
middlewares.ApplyDefaultCORS(corsAllowOrigin),
|
||||
))
|
||||
app.Options("/delete-user",
|
||||
middlewares.ApplyDefaultCORSPreflight(corsAllowOrigin),
|
||||
middlewares.ApplyDefaultCORS(corsAllowOrigin),
|
||||
)
|
||||
|
||||
// UpdateUser admin api
|
||||
app.Patch("/update-user",
|
||||
controllers.ProcessHandlers(adminController.UpdateUser, metrics.ActionAdminUpdateUser, adminServices,
|
||||
middlewares.VerifyV4Signature(root, iam, region, false, true),
|
||||
middlewares.IsAdmin(metrics.ActionAdminUpdateUser),
|
||||
middlewares.ApplyDefaultCORS(corsAllowOrigin),
|
||||
))
|
||||
app.Options("/update-user",
|
||||
middlewares.ApplyDefaultCORSPreflight(corsAllowOrigin),
|
||||
middlewares.ApplyDefaultCORS(corsAllowOrigin),
|
||||
)
|
||||
|
||||
// ListUsers admin api
|
||||
app.Patch("/list-users",
|
||||
controllers.ProcessHandlers(adminController.ListUsers, metrics.ActionAdminListUsers, adminServices,
|
||||
middlewares.VerifyV4Signature(root, iam, region, false, true),
|
||||
middlewares.IsAdmin(metrics.ActionAdminListUsers),
|
||||
middlewares.ApplyDefaultCORS(corsAllowOrigin),
|
||||
))
|
||||
app.Options("/list-users",
|
||||
middlewares.ApplyDefaultCORSPreflight(corsAllowOrigin),
|
||||
middlewares.ApplyDefaultCORS(corsAllowOrigin),
|
||||
)
|
||||
|
||||
// ChangeBucketOwner admin api
|
||||
app.Patch("/change-bucket-owner",
|
||||
controllers.ProcessHandlers(adminController.ChangeBucketOwner, metrics.ActionAdminChangeBucketOwner, adminServices,
|
||||
middlewares.VerifyV4Signature(root, iam, region, false, true),
|
||||
middlewares.IsAdmin(metrics.ActionAdminChangeBucketOwner),
|
||||
middlewares.ApplyDefaultCORS(corsAllowOrigin),
|
||||
))
|
||||
app.Options("/change-bucket-owner",
|
||||
middlewares.ApplyDefaultCORSPreflight(corsAllowOrigin),
|
||||
middlewares.ApplyDefaultCORS(corsAllowOrigin),
|
||||
)
|
||||
|
||||
// ListBucketsAndOwners admin api
|
||||
app.Patch("/list-buckets",
|
||||
controllers.ProcessHandlers(adminController.ListBuckets, metrics.ActionAdminListBuckets, adminServices,
|
||||
middlewares.VerifyV4Signature(root, iam, region, false, true),
|
||||
middlewares.IsAdmin(metrics.ActionAdminListBuckets),
|
||||
middlewares.ApplyDefaultCORS(corsAllowOrigin),
|
||||
))
|
||||
app.Options("/list-buckets",
|
||||
middlewares.ApplyDefaultCORSPreflight(corsAllowOrigin),
|
||||
middlewares.ApplyDefaultCORS(corsAllowOrigin),
|
||||
)
|
||||
}
|
||||
|
||||
services := &controllers.Services{
|
||||
@@ -92,7 +122,12 @@ func (sa *S3ApiRouter) Init(app *fiber.App, be backend.Backend, iam auth.IAMServ
|
||||
|
||||
// copy source is not allowed on '/'
|
||||
app.Get("/", middlewares.MatchHeader("X-Amz-Copy-Source"),
|
||||
controllers.ProcessHandlers(ctrl.HandleErrorRoute(s3err.GetAPIError(s3err.ErrCopySourceNotAllowed)), metrics.ActionUndetected, services),
|
||||
controllers.ProcessHandlers(
|
||||
ctrl.HandleErrorRoute(s3err.GetAPIError(s3err.ErrCopySourceNotAllowed)),
|
||||
metrics.ActionUndetected,
|
||||
services,
|
||||
middlewares.ApplyDefaultCORS(corsAllowOrigin),
|
||||
),
|
||||
)
|
||||
|
||||
app.Get("/",
|
||||
@@ -100,11 +135,17 @@ func (sa *S3ApiRouter) Init(app *fiber.App, be backend.Backend, iam auth.IAMServ
|
||||
ctrl.ListBuckets,
|
||||
metrics.ActionListAllMyBuckets,
|
||||
services,
|
||||
middlewares.ApplyDefaultCORS(corsAllowOrigin),
|
||||
middlewares.AuthorizePublicBucketAccess(be, metrics.ActionListAllMyBuckets, "", auth.PermissionRead, region, false),
|
||||
middlewares.VerifyPresignedV4Signature(root, iam, region, false),
|
||||
middlewares.VerifyV4Signature(root, iam, region, false, true),
|
||||
))
|
||||
|
||||
app.Options("/",
|
||||
middlewares.ApplyDefaultCORSPreflight(corsAllowOrigin),
|
||||
middlewares.ApplyDefaultCORS(corsAllowOrigin),
|
||||
)
|
||||
|
||||
bucketRouter := app.Group("/:bucket")
|
||||
objectRouter := app.Group("/:bucket/*")
|
||||
|
||||
@@ -116,12 +157,12 @@ func (sa *S3ApiRouter) Init(app *fiber.App, be backend.Backend, iam auth.IAMServ
|
||||
metrics.ActionPutBucketTagging,
|
||||
services,
|
||||
middlewares.BucketObjectNameValidator(),
|
||||
middlewares.ApplyBucketCORS(be, corsAllowOrigin),
|
||||
middlewares.AuthorizePublicBucketAccess(be, metrics.ActionPutBucketTagging, auth.PutBucketTaggingAction, auth.PermissionWrite, region, false),
|
||||
middlewares.VerifyPresignedV4Signature(root, iam, region, false),
|
||||
middlewares.VerifyV4Signature(root, iam, region, false, true),
|
||||
middlewares.VerifyChecksums(false, true, true),
|
||||
middlewares.ParseAcl(be),
|
||||
middlewares.ApplyBucketCORS(be),
|
||||
))
|
||||
bucketRouter.Put("",
|
||||
middlewares.MatchQueryArgs("ownershipControls"),
|
||||
@@ -134,7 +175,7 @@ func (sa *S3ApiRouter) Init(app *fiber.App, be backend.Backend, iam auth.IAMServ
|
||||
middlewares.VerifyPresignedV4Signature(root, iam, region, false),
|
||||
middlewares.VerifyV4Signature(root, iam, region, false, true),
|
||||
middlewares.VerifyChecksums(false, true, false),
|
||||
middlewares.ApplyBucketCORS(be),
|
||||
middlewares.ApplyBucketCORS(be, corsAllowOrigin),
|
||||
middlewares.ParseAcl(be),
|
||||
))
|
||||
bucketRouter.Put("",
|
||||
@@ -148,7 +189,7 @@ func (sa *S3ApiRouter) Init(app *fiber.App, be backend.Backend, iam auth.IAMServ
|
||||
middlewares.VerifyPresignedV4Signature(root, iam, region, false),
|
||||
middlewares.VerifyV4Signature(root, iam, region, false, true),
|
||||
middlewares.VerifyChecksums(false, true, false),
|
||||
middlewares.ApplyBucketCORS(be),
|
||||
middlewares.ApplyBucketCORS(be, corsAllowOrigin),
|
||||
middlewares.ParseAcl(be),
|
||||
))
|
||||
bucketRouter.Put("",
|
||||
@@ -162,7 +203,7 @@ func (sa *S3ApiRouter) Init(app *fiber.App, be backend.Backend, iam auth.IAMServ
|
||||
middlewares.VerifyPresignedV4Signature(root, iam, region, false),
|
||||
middlewares.VerifyV4Signature(root, iam, region, false, true),
|
||||
middlewares.VerifyChecksums(false, true, true),
|
||||
middlewares.ApplyBucketCORS(be),
|
||||
middlewares.ApplyBucketCORS(be, corsAllowOrigin),
|
||||
middlewares.ParseAcl(be),
|
||||
))
|
||||
bucketRouter.Put("",
|
||||
@@ -176,7 +217,7 @@ func (sa *S3ApiRouter) Init(app *fiber.App, be backend.Backend, iam auth.IAMServ
|
||||
middlewares.VerifyPresignedV4Signature(root, iam, region, false),
|
||||
middlewares.VerifyV4Signature(root, iam, region, false, true),
|
||||
middlewares.VerifyChecksums(false, true, true),
|
||||
middlewares.ApplyBucketCORS(be),
|
||||
middlewares.ApplyBucketCORS(be, corsAllowOrigin),
|
||||
middlewares.ParseAcl(be),
|
||||
))
|
||||
bucketRouter.Put("",
|
||||
@@ -190,7 +231,7 @@ func (sa *S3ApiRouter) Init(app *fiber.App, be backend.Backend, iam auth.IAMServ
|
||||
middlewares.VerifyPresignedV4Signature(root, iam, region, false),
|
||||
middlewares.VerifyV4Signature(root, iam, region, false, true),
|
||||
middlewares.VerifyChecksums(false, false, false),
|
||||
middlewares.ApplyBucketCORS(be),
|
||||
middlewares.ApplyBucketCORS(be, corsAllowOrigin),
|
||||
middlewares.ParseAcl(be),
|
||||
))
|
||||
bucketRouter.Put("",
|
||||
@@ -204,7 +245,7 @@ func (sa *S3ApiRouter) Init(app *fiber.App, be backend.Backend, iam auth.IAMServ
|
||||
middlewares.VerifyPresignedV4Signature(root, iam, region, false),
|
||||
middlewares.VerifyV4Signature(root, iam, region, false, true),
|
||||
middlewares.VerifyChecksums(false, false, false),
|
||||
middlewares.ApplyBucketCORS(be),
|
||||
middlewares.ApplyBucketCORS(be, corsAllowOrigin),
|
||||
middlewares.ParseAcl(be),
|
||||
))
|
||||
bucketRouter.Put("",
|
||||
@@ -386,7 +427,7 @@ func (sa *S3ApiRouter) Init(app *fiber.App, be backend.Backend, iam auth.IAMServ
|
||||
middlewares.VerifyPresignedV4Signature(root, iam, region, false),
|
||||
middlewares.VerifyV4Signature(root, iam, region, false, true),
|
||||
middlewares.VerifyChecksums(false, false, false),
|
||||
middlewares.ApplyBucketCORS(be),
|
||||
middlewares.ApplyBucketCORS(be, corsAllowOrigin),
|
||||
))
|
||||
|
||||
// HeadBucket action
|
||||
@@ -401,12 +442,11 @@ func (sa *S3ApiRouter) Init(app *fiber.App, be backend.Backend, iam auth.IAMServ
|
||||
ctrl.HeadBucket,
|
||||
metrics.ActionHeadBucket,
|
||||
services,
|
||||
middlewares.ApplyBucketCORS(be),
|
||||
middlewares.BucketObjectNameValidator(),
|
||||
middlewares.AuthorizePublicBucketAccess(be, metrics.ActionHeadBucket, auth.ListBucketAction, auth.PermissionRead, region, false),
|
||||
middlewares.VerifyPresignedV4Signature(root, iam, region, false),
|
||||
middlewares.VerifyV4Signature(root, iam, region, false, false),
|
||||
middlewares.ApplyBucketCORS(be),
|
||||
middlewares.ApplyBucketCORS(be, corsAllowOrigin),
|
||||
middlewares.ParseAcl(be),
|
||||
))
|
||||
|
||||
@@ -427,7 +467,7 @@ func (sa *S3ApiRouter) Init(app *fiber.App, be backend.Backend, iam auth.IAMServ
|
||||
middlewares.AuthorizePublicBucketAccess(be, metrics.ActionDeleteBucketTagging, auth.PutBucketTaggingAction, auth.PermissionWrite, region, false),
|
||||
middlewares.VerifyPresignedV4Signature(root, iam, region, false),
|
||||
middlewares.VerifyV4Signature(root, iam, region, false, true),
|
||||
middlewares.ApplyBucketCORS(be),
|
||||
middlewares.ApplyBucketCORS(be, corsAllowOrigin),
|
||||
middlewares.ParseAcl(be),
|
||||
))
|
||||
bucketRouter.Delete("",
|
||||
@@ -440,7 +480,7 @@ func (sa *S3ApiRouter) Init(app *fiber.App, be backend.Backend, iam auth.IAMServ
|
||||
middlewares.AuthorizePublicBucketAccess(be, metrics.ActionDeleteBucketOwnershipControls, auth.PutBucketOwnershipControlsAction, auth.PermissionWrite, region, false),
|
||||
middlewares.VerifyPresignedV4Signature(root, iam, region, false),
|
||||
middlewares.VerifyV4Signature(root, iam, region, false, true),
|
||||
middlewares.ApplyBucketCORS(be),
|
||||
middlewares.ApplyBucketCORS(be, corsAllowOrigin),
|
||||
middlewares.ParseAcl(be),
|
||||
))
|
||||
bucketRouter.Delete("",
|
||||
@@ -453,7 +493,7 @@ func (sa *S3ApiRouter) Init(app *fiber.App, be backend.Backend, iam auth.IAMServ
|
||||
middlewares.AuthorizePublicBucketAccess(be, metrics.ActionDeleteBucketPolicy, auth.PutBucketPolicyAction, auth.PermissionWrite, region, false),
|
||||
middlewares.VerifyPresignedV4Signature(root, iam, region, false),
|
||||
middlewares.VerifyV4Signature(root, iam, region, false, true),
|
||||
middlewares.ApplyBucketCORS(be),
|
||||
middlewares.ApplyBucketCORS(be, corsAllowOrigin),
|
||||
middlewares.ParseAcl(be),
|
||||
))
|
||||
bucketRouter.Delete("",
|
||||
@@ -466,7 +506,7 @@ func (sa *S3ApiRouter) Init(app *fiber.App, be backend.Backend, iam auth.IAMServ
|
||||
middlewares.AuthorizePublicBucketAccess(be, metrics.ActionDeleteBucketCors, auth.PutBucketCorsAction, auth.PermissionWrite, region, false),
|
||||
middlewares.VerifyPresignedV4Signature(root, iam, region, false),
|
||||
middlewares.VerifyV4Signature(root, iam, region, false, true),
|
||||
middlewares.ApplyBucketCORS(be),
|
||||
middlewares.ApplyBucketCORS(be, corsAllowOrigin),
|
||||
middlewares.ParseAcl(be),
|
||||
))
|
||||
bucketRouter.Delete("",
|
||||
@@ -595,7 +635,7 @@ func (sa *S3ApiRouter) Init(app *fiber.App, be backend.Backend, iam auth.IAMServ
|
||||
middlewares.AuthorizePublicBucketAccess(be, metrics.ActionDeleteBucket, auth.DeleteBucketAction, auth.PermissionWrite, region, false),
|
||||
middlewares.VerifyPresignedV4Signature(root, iam, region, false),
|
||||
middlewares.VerifyV4Signature(root, iam, region, false, true),
|
||||
middlewares.ApplyBucketCORS(be),
|
||||
middlewares.ApplyBucketCORS(be, corsAllowOrigin),
|
||||
middlewares.ParseAcl(be),
|
||||
))
|
||||
|
||||
@@ -616,7 +656,7 @@ func (sa *S3ApiRouter) Init(app *fiber.App, be backend.Backend, iam auth.IAMServ
|
||||
middlewares.AuthorizePublicBucketAccess(be, metrics.ActionGetBucketLocation, auth.GetBucketLocationAction, auth.PermissionRead, region, false),
|
||||
middlewares.VerifyPresignedV4Signature(root, iam, region, false),
|
||||
middlewares.VerifyV4Signature(root, iam, region, false, true),
|
||||
middlewares.ApplyBucketCORS(be),
|
||||
middlewares.ApplyBucketCORS(be, corsAllowOrigin),
|
||||
middlewares.ParseAcl(be),
|
||||
),
|
||||
)
|
||||
@@ -630,7 +670,7 @@ func (sa *S3ApiRouter) Init(app *fiber.App, be backend.Backend, iam auth.IAMServ
|
||||
middlewares.AuthorizePublicBucketAccess(be, metrics.ActionGetBucketTagging, auth.GetBucketTaggingAction, auth.PermissionRead, region, false),
|
||||
middlewares.VerifyPresignedV4Signature(root, iam, region, false),
|
||||
middlewares.VerifyV4Signature(root, iam, region, false, true),
|
||||
middlewares.ApplyBucketCORS(be),
|
||||
middlewares.ApplyBucketCORS(be, corsAllowOrigin),
|
||||
middlewares.ParseAcl(be),
|
||||
))
|
||||
bucketRouter.Get("",
|
||||
@@ -643,7 +683,7 @@ func (sa *S3ApiRouter) Init(app *fiber.App, be backend.Backend, iam auth.IAMServ
|
||||
middlewares.AuthorizePublicBucketAccess(be, metrics.ActionGetBucketOwnershipControls, auth.GetBucketOwnershipControlsAction, auth.PermissionRead, region, false),
|
||||
middlewares.VerifyPresignedV4Signature(root, iam, region, false),
|
||||
middlewares.VerifyV4Signature(root, iam, region, false, true),
|
||||
middlewares.ApplyBucketCORS(be),
|
||||
middlewares.ApplyBucketCORS(be, corsAllowOrigin),
|
||||
middlewares.ParseAcl(be),
|
||||
))
|
||||
bucketRouter.Get("",
|
||||
@@ -656,7 +696,7 @@ func (sa *S3ApiRouter) Init(app *fiber.App, be backend.Backend, iam auth.IAMServ
|
||||
middlewares.AuthorizePublicBucketAccess(be, metrics.ActionGetBucketVersioning, auth.GetBucketVersioningAction, auth.PermissionRead, region, false),
|
||||
middlewares.VerifyPresignedV4Signature(root, iam, region, false),
|
||||
middlewares.VerifyV4Signature(root, iam, region, false, true),
|
||||
middlewares.ApplyBucketCORS(be),
|
||||
middlewares.ApplyBucketCORS(be, corsAllowOrigin),
|
||||
middlewares.ParseAcl(be),
|
||||
))
|
||||
bucketRouter.Get("",
|
||||
@@ -669,7 +709,7 @@ func (sa *S3ApiRouter) Init(app *fiber.App, be backend.Backend, iam auth.IAMServ
|
||||
middlewares.AuthorizePublicBucketAccess(be, metrics.ActionGetBucketPolicy, auth.GetBucketPolicyAction, auth.PermissionRead, region, false),
|
||||
middlewares.VerifyPresignedV4Signature(root, iam, region, false),
|
||||
middlewares.VerifyV4Signature(root, iam, region, false, true),
|
||||
middlewares.ApplyBucketCORS(be),
|
||||
middlewares.ApplyBucketCORS(be, corsAllowOrigin),
|
||||
middlewares.ParseAcl(be),
|
||||
))
|
||||
bucketRouter.Get("",
|
||||
@@ -682,7 +722,7 @@ func (sa *S3ApiRouter) Init(app *fiber.App, be backend.Backend, iam auth.IAMServ
|
||||
middlewares.AuthorizePublicBucketAccess(be, metrics.ActionGetBucketCors, auth.GetBucketCorsAction, auth.PermissionRead, region, false),
|
||||
middlewares.VerifyPresignedV4Signature(root, iam, region, false),
|
||||
middlewares.VerifyV4Signature(root, iam, region, false, true),
|
||||
middlewares.ApplyBucketCORS(be),
|
||||
middlewares.ApplyBucketCORS(be, corsAllowOrigin),
|
||||
middlewares.ParseAcl(be),
|
||||
))
|
||||
bucketRouter.Get("",
|
||||
@@ -695,7 +735,7 @@ func (sa *S3ApiRouter) Init(app *fiber.App, be backend.Backend, iam auth.IAMServ
|
||||
middlewares.AuthorizePublicBucketAccess(be, metrics.ActionGetObjectLockConfiguration, auth.GetBucketObjectLockConfigurationAction, auth.PermissionRead, region, false),
|
||||
middlewares.VerifyPresignedV4Signature(root, iam, region, false),
|
||||
middlewares.VerifyV4Signature(root, iam, region, false, true),
|
||||
middlewares.ApplyBucketCORS(be),
|
||||
middlewares.ApplyBucketCORS(be, corsAllowOrigin),
|
||||
middlewares.ParseAcl(be),
|
||||
))
|
||||
bucketRouter.Get("",
|
||||
@@ -708,7 +748,7 @@ func (sa *S3ApiRouter) Init(app *fiber.App, be backend.Backend, iam auth.IAMServ
|
||||
middlewares.AuthorizePublicBucketAccess(be, metrics.ActionGetBucketAcl, auth.GetBucketAclAction, auth.PermissionReadAcp, region, false),
|
||||
middlewares.VerifyPresignedV4Signature(root, iam, region, false),
|
||||
middlewares.VerifyV4Signature(root, iam, region, false, false),
|
||||
middlewares.ApplyBucketCORS(be),
|
||||
middlewares.ApplyBucketCORS(be, corsAllowOrigin),
|
||||
middlewares.ParseAcl(be),
|
||||
))
|
||||
bucketRouter.Get("",
|
||||
@@ -721,7 +761,7 @@ func (sa *S3ApiRouter) Init(app *fiber.App, be backend.Backend, iam auth.IAMServ
|
||||
middlewares.AuthorizePublicBucketAccess(be, metrics.ActionListMultipartUploads, auth.ListBucketMultipartUploadsAction, auth.PermissionRead, region, false),
|
||||
middlewares.VerifyPresignedV4Signature(root, iam, region, false),
|
||||
middlewares.VerifyV4Signature(root, iam, region, false, true),
|
||||
middlewares.ApplyBucketCORS(be),
|
||||
middlewares.ApplyBucketCORS(be, corsAllowOrigin),
|
||||
middlewares.ParseAcl(be),
|
||||
))
|
||||
bucketRouter.Get("",
|
||||
@@ -734,7 +774,7 @@ func (sa *S3ApiRouter) Init(app *fiber.App, be backend.Backend, iam auth.IAMServ
|
||||
middlewares.AuthorizePublicBucketAccess(be, metrics.ActionListObjectVersions, auth.ListBucketVersionsAction, auth.PermissionRead, region, false),
|
||||
middlewares.VerifyPresignedV4Signature(root, iam, region, false),
|
||||
middlewares.VerifyV4Signature(root, iam, region, false, true),
|
||||
middlewares.ApplyBucketCORS(be),
|
||||
middlewares.ApplyBucketCORS(be, corsAllowOrigin),
|
||||
middlewares.ParseAcl(be),
|
||||
))
|
||||
bucketRouter.Get("",
|
||||
@@ -747,7 +787,7 @@ func (sa *S3ApiRouter) Init(app *fiber.App, be backend.Backend, iam auth.IAMServ
|
||||
middlewares.AuthorizePublicBucketAccess(be, metrics.ActionGetBucketPolicyStatus, auth.GetBucketPolicyStatusAction, auth.PermissionRead, region, false),
|
||||
middlewares.VerifyPresignedV4Signature(root, iam, region, false),
|
||||
middlewares.VerifyV4Signature(root, iam, region, false, true),
|
||||
middlewares.ApplyBucketCORS(be),
|
||||
middlewares.ApplyBucketCORS(be, corsAllowOrigin),
|
||||
middlewares.ParseAcl(be),
|
||||
))
|
||||
bucketRouter.Get("",
|
||||
@@ -981,7 +1021,7 @@ func (sa *S3ApiRouter) Init(app *fiber.App, be backend.Backend, iam auth.IAMServ
|
||||
middlewares.AuthorizePublicBucketAccess(be, metrics.ActionListObjectsV2, auth.ListBucketAction, auth.PermissionRead, region, false),
|
||||
middlewares.VerifyPresignedV4Signature(root, iam, region, false),
|
||||
middlewares.VerifyV4Signature(root, iam, region, false, true),
|
||||
middlewares.ApplyBucketCORS(be),
|
||||
middlewares.ApplyBucketCORS(be, corsAllowOrigin),
|
||||
middlewares.ParseAcl(be),
|
||||
))
|
||||
bucketRouter.Get("",
|
||||
@@ -993,7 +1033,7 @@ func (sa *S3ApiRouter) Init(app *fiber.App, be backend.Backend, iam auth.IAMServ
|
||||
middlewares.AuthorizePublicBucketAccess(be, metrics.ActionListObjects, auth.ListBucketAction, auth.PermissionRead, region, false),
|
||||
middlewares.VerifyPresignedV4Signature(root, iam, region, false),
|
||||
middlewares.VerifyV4Signature(root, iam, region, false, true),
|
||||
middlewares.ApplyBucketCORS(be),
|
||||
middlewares.ApplyBucketCORS(be, corsAllowOrigin),
|
||||
middlewares.ParseAcl(be),
|
||||
))
|
||||
|
||||
@@ -1016,7 +1056,7 @@ func (sa *S3ApiRouter) Init(app *fiber.App, be backend.Backend, iam auth.IAMServ
|
||||
middlewares.VerifyPresignedV4Signature(root, iam, region, false),
|
||||
middlewares.VerifyV4Signature(root, iam, region, false, true),
|
||||
middlewares.VerifyChecksums(false, true, true),
|
||||
middlewares.ApplyBucketCORS(be),
|
||||
middlewares.ApplyBucketCORS(be, corsAllowOrigin),
|
||||
middlewares.ParseAcl(be),
|
||||
))
|
||||
|
||||
@@ -1036,7 +1076,7 @@ func (sa *S3ApiRouter) Init(app *fiber.App, be backend.Backend, iam auth.IAMServ
|
||||
middlewares.AuthorizePublicBucketAccess(be, metrics.ActionHeadObject, auth.GetObjectAction, auth.PermissionRead, region, false),
|
||||
middlewares.VerifyPresignedV4Signature(root, iam, region, false),
|
||||
middlewares.VerifyV4Signature(root, iam, region, false, false),
|
||||
middlewares.ApplyBucketCORS(be),
|
||||
middlewares.ApplyBucketCORS(be, corsAllowOrigin),
|
||||
middlewares.ParseAcl(be),
|
||||
))
|
||||
|
||||
@@ -1070,7 +1110,7 @@ func (sa *S3ApiRouter) Init(app *fiber.App, be backend.Backend, iam auth.IAMServ
|
||||
middlewares.AuthorizePublicBucketAccess(be, metrics.ActionGetObjectTagging, auth.GetObjectTaggingAction, auth.PermissionRead, region, false),
|
||||
middlewares.VerifyPresignedV4Signature(root, iam, region, false),
|
||||
middlewares.VerifyV4Signature(root, iam, region, false, true),
|
||||
middlewares.ApplyBucketCORS(be),
|
||||
middlewares.ApplyBucketCORS(be, corsAllowOrigin),
|
||||
middlewares.ParseAcl(be),
|
||||
))
|
||||
objectRouter.Get("",
|
||||
@@ -1083,7 +1123,7 @@ func (sa *S3ApiRouter) Init(app *fiber.App, be backend.Backend, iam auth.IAMServ
|
||||
middlewares.AuthorizePublicBucketAccess(be, metrics.ActionGetObjectRetention, auth.GetObjectRetentionAction, auth.PermissionRead, region, false),
|
||||
middlewares.VerifyPresignedV4Signature(root, iam, region, false),
|
||||
middlewares.VerifyV4Signature(root, iam, region, false, true),
|
||||
middlewares.ApplyBucketCORS(be),
|
||||
middlewares.ApplyBucketCORS(be, corsAllowOrigin),
|
||||
middlewares.ParseAcl(be),
|
||||
))
|
||||
objectRouter.Get("",
|
||||
@@ -1096,7 +1136,7 @@ func (sa *S3ApiRouter) Init(app *fiber.App, be backend.Backend, iam auth.IAMServ
|
||||
middlewares.AuthorizePublicBucketAccess(be, metrics.ActionGetObjectLegalHold, auth.GetObjectLegalHoldAction, auth.PermissionRead, region, false),
|
||||
middlewares.VerifyPresignedV4Signature(root, iam, region, false),
|
||||
middlewares.VerifyV4Signature(root, iam, region, false, true),
|
||||
middlewares.ApplyBucketCORS(be),
|
||||
middlewares.ApplyBucketCORS(be, corsAllowOrigin),
|
||||
middlewares.ParseAcl(be),
|
||||
))
|
||||
objectRouter.Get("",
|
||||
@@ -1109,7 +1149,7 @@ func (sa *S3ApiRouter) Init(app *fiber.App, be backend.Backend, iam auth.IAMServ
|
||||
middlewares.AuthorizePublicBucketAccess(be, metrics.ActionGetObjectAcl, auth.GetObjectAclAction, auth.PermissionReadAcp, region, false),
|
||||
middlewares.VerifyPresignedV4Signature(root, iam, region, false),
|
||||
middlewares.VerifyV4Signature(root, iam, region, false, true),
|
||||
middlewares.ApplyBucketCORS(be),
|
||||
middlewares.ApplyBucketCORS(be, corsAllowOrigin),
|
||||
middlewares.ParseAcl(be),
|
||||
))
|
||||
objectRouter.Get("",
|
||||
@@ -1122,7 +1162,7 @@ func (sa *S3ApiRouter) Init(app *fiber.App, be backend.Backend, iam auth.IAMServ
|
||||
middlewares.AuthorizePublicBucketAccess(be, metrics.ActionGetObjectAttributes, auth.GetObjectAttributesAction, auth.PermissionRead, region, false),
|
||||
middlewares.VerifyPresignedV4Signature(root, iam, region, false),
|
||||
middlewares.VerifyV4Signature(root, iam, region, false, true),
|
||||
middlewares.ApplyBucketCORS(be),
|
||||
middlewares.ApplyBucketCORS(be, corsAllowOrigin),
|
||||
middlewares.ParseAcl(be),
|
||||
))
|
||||
objectRouter.Get("",
|
||||
@@ -1135,7 +1175,7 @@ func (sa *S3ApiRouter) Init(app *fiber.App, be backend.Backend, iam auth.IAMServ
|
||||
middlewares.AuthorizePublicBucketAccess(be, metrics.ActionListParts, auth.ListMultipartUploadPartsAction, auth.PermissionRead, region, false),
|
||||
middlewares.VerifyPresignedV4Signature(root, iam, region, false),
|
||||
middlewares.VerifyV4Signature(root, iam, region, false, true),
|
||||
middlewares.ApplyBucketCORS(be),
|
||||
middlewares.ApplyBucketCORS(be, corsAllowOrigin),
|
||||
middlewares.ParseAcl(be),
|
||||
))
|
||||
objectRouter.Get("",
|
||||
@@ -1147,7 +1187,7 @@ func (sa *S3ApiRouter) Init(app *fiber.App, be backend.Backend, iam auth.IAMServ
|
||||
middlewares.AuthorizePublicBucketAccess(be, metrics.ActionGetObject, auth.GetObjectAction, auth.PermissionRead, region, false),
|
||||
middlewares.VerifyPresignedV4Signature(root, iam, region, false),
|
||||
middlewares.VerifyV4Signature(root, iam, region, false, true),
|
||||
middlewares.ApplyBucketCORS(be),
|
||||
middlewares.ApplyBucketCORS(be, corsAllowOrigin),
|
||||
middlewares.ParseAcl(be),
|
||||
))
|
||||
|
||||
@@ -1169,7 +1209,7 @@ func (sa *S3ApiRouter) Init(app *fiber.App, be backend.Backend, iam auth.IAMServ
|
||||
middlewares.AuthorizePublicBucketAccess(be, metrics.ActionDeleteObjectTagging, auth.DeleteObjectTaggingAction, auth.PermissionWrite, region, false),
|
||||
middlewares.VerifyPresignedV4Signature(root, iam, region, false),
|
||||
middlewares.VerifyV4Signature(root, iam, region, false, true),
|
||||
middlewares.ApplyBucketCORS(be),
|
||||
middlewares.ApplyBucketCORS(be, corsAllowOrigin),
|
||||
middlewares.ParseAcl(be),
|
||||
))
|
||||
objectRouter.Delete("",
|
||||
@@ -1182,7 +1222,7 @@ func (sa *S3ApiRouter) Init(app *fiber.App, be backend.Backend, iam auth.IAMServ
|
||||
middlewares.AuthorizePublicBucketAccess(be, metrics.ActionAbortMultipartUpload, auth.AbortMultipartUploadAction, auth.PermissionWrite, region, false),
|
||||
middlewares.VerifyPresignedV4Signature(root, iam, region, false),
|
||||
middlewares.VerifyV4Signature(root, iam, region, false, true),
|
||||
middlewares.ApplyBucketCORS(be),
|
||||
middlewares.ApplyBucketCORS(be, corsAllowOrigin),
|
||||
middlewares.ParseAcl(be),
|
||||
))
|
||||
objectRouter.Delete("",
|
||||
@@ -1194,7 +1234,7 @@ func (sa *S3ApiRouter) Init(app *fiber.App, be backend.Backend, iam auth.IAMServ
|
||||
middlewares.AuthorizePublicBucketAccess(be, metrics.ActionDeleteObject, auth.DeleteObjectAction, auth.PermissionWrite, region, false),
|
||||
middlewares.VerifyPresignedV4Signature(root, iam, region, false),
|
||||
middlewares.VerifyV4Signature(root, iam, region, false, true),
|
||||
middlewares.ApplyBucketCORS(be),
|
||||
middlewares.ApplyBucketCORS(be, corsAllowOrigin),
|
||||
middlewares.ParseAcl(be),
|
||||
))
|
||||
|
||||
@@ -1218,7 +1258,7 @@ func (sa *S3ApiRouter) Init(app *fiber.App, be backend.Backend, iam auth.IAMServ
|
||||
middlewares.VerifyPresignedV4Signature(root, iam, region, false),
|
||||
middlewares.VerifyV4Signature(root, iam, region, false, true),
|
||||
middlewares.VerifyChecksums(false, false, false),
|
||||
middlewares.ApplyBucketCORS(be),
|
||||
middlewares.ApplyBucketCORS(be, corsAllowOrigin),
|
||||
middlewares.ParseAcl(be),
|
||||
))
|
||||
objectRouter.Post("",
|
||||
@@ -1233,7 +1273,7 @@ func (sa *S3ApiRouter) Init(app *fiber.App, be backend.Backend, iam auth.IAMServ
|
||||
middlewares.VerifyPresignedV4Signature(root, iam, region, false),
|
||||
middlewares.VerifyV4Signature(root, iam, region, false, true),
|
||||
middlewares.VerifyChecksums(false, false, false),
|
||||
middlewares.ApplyBucketCORS(be),
|
||||
middlewares.ApplyBucketCORS(be, corsAllowOrigin),
|
||||
middlewares.ParseAcl(be),
|
||||
))
|
||||
objectRouter.Post("",
|
||||
@@ -1246,7 +1286,7 @@ func (sa *S3ApiRouter) Init(app *fiber.App, be backend.Backend, iam auth.IAMServ
|
||||
middlewares.AuthorizePublicBucketAccess(be, metrics.ActionCompleteMultipartUpload, auth.PutObjectAction, auth.PermissionWrite, region, false),
|
||||
middlewares.VerifyPresignedV4Signature(root, iam, region, false),
|
||||
middlewares.VerifyV4Signature(root, iam, region, false, true),
|
||||
middlewares.ApplyBucketCORS(be),
|
||||
middlewares.ApplyBucketCORS(be, corsAllowOrigin),
|
||||
middlewares.ParseAcl(be),
|
||||
))
|
||||
objectRouter.Post("",
|
||||
@@ -1259,7 +1299,7 @@ func (sa *S3ApiRouter) Init(app *fiber.App, be backend.Backend, iam auth.IAMServ
|
||||
middlewares.AuthorizePublicBucketAccess(be, metrics.ActionCreateMultipartUpload, auth.PutObjectAction, auth.PermissionWrite, region, false),
|
||||
middlewares.VerifyPresignedV4Signature(root, iam, region, false),
|
||||
middlewares.VerifyV4Signature(root, iam, region, false, true),
|
||||
middlewares.ApplyBucketCORS(be),
|
||||
middlewares.ApplyBucketCORS(be, corsAllowOrigin),
|
||||
middlewares.ParseAcl(be),
|
||||
))
|
||||
|
||||
@@ -1271,11 +1311,11 @@ func (sa *S3ApiRouter) Init(app *fiber.App, be backend.Backend, iam auth.IAMServ
|
||||
metrics.ActionPutObjectTagging,
|
||||
services,
|
||||
middlewares.BucketObjectNameValidator(),
|
||||
middlewares.ApplyBucketCORS(be, corsAllowOrigin),
|
||||
middlewares.AuthorizePublicBucketAccess(be, metrics.ActionPutObjectTagging, auth.PutObjectTaggingAction, auth.PermissionWrite, region, false),
|
||||
middlewares.VerifyPresignedV4Signature(root, iam, region, false),
|
||||
middlewares.VerifyV4Signature(root, iam, region, false, true),
|
||||
middlewares.VerifyChecksums(false, true, false),
|
||||
middlewares.ApplyBucketCORS(be),
|
||||
middlewares.ParseAcl(be),
|
||||
))
|
||||
objectRouter.Put("",
|
||||
@@ -1289,7 +1329,7 @@ func (sa *S3ApiRouter) Init(app *fiber.App, be backend.Backend, iam auth.IAMServ
|
||||
middlewares.VerifyPresignedV4Signature(root, iam, region, false),
|
||||
middlewares.VerifyV4Signature(root, iam, region, false, true),
|
||||
middlewares.VerifyChecksums(false, false, true),
|
||||
middlewares.ApplyBucketCORS(be),
|
||||
middlewares.ApplyBucketCORS(be, corsAllowOrigin),
|
||||
middlewares.ParseAcl(be),
|
||||
))
|
||||
objectRouter.Put("",
|
||||
@@ -1303,7 +1343,7 @@ func (sa *S3ApiRouter) Init(app *fiber.App, be backend.Backend, iam auth.IAMServ
|
||||
middlewares.VerifyPresignedV4Signature(root, iam, region, false),
|
||||
middlewares.VerifyV4Signature(root, iam, region, false, true),
|
||||
middlewares.VerifyChecksums(false, false, true),
|
||||
middlewares.ApplyBucketCORS(be),
|
||||
middlewares.ApplyBucketCORS(be, corsAllowOrigin),
|
||||
middlewares.ParseAcl(be),
|
||||
))
|
||||
objectRouter.Put("",
|
||||
@@ -1317,7 +1357,7 @@ func (sa *S3ApiRouter) Init(app *fiber.App, be backend.Backend, iam auth.IAMServ
|
||||
middlewares.VerifyPresignedV4Signature(root, iam, region, false),
|
||||
middlewares.VerifyV4Signature(root, iam, region, false, true),
|
||||
middlewares.VerifyChecksums(false, false, false),
|
||||
middlewares.ApplyBucketCORS(be),
|
||||
middlewares.ApplyBucketCORS(be, corsAllowOrigin),
|
||||
middlewares.ParseAcl(be),
|
||||
))
|
||||
objectRouter.Put("",
|
||||
@@ -1331,7 +1371,7 @@ func (sa *S3ApiRouter) Init(app *fiber.App, be backend.Backend, iam auth.IAMServ
|
||||
middlewares.AuthorizePublicBucketAccess(be, metrics.ActionUploadPartCopy, auth.PutObjectAction, auth.PermissionWrite, region, false),
|
||||
middlewares.VerifyPresignedV4Signature(root, iam, region, false),
|
||||
middlewares.VerifyV4Signature(root, iam, region, false, true),
|
||||
middlewares.ApplyBucketCORS(be),
|
||||
middlewares.ApplyBucketCORS(be, corsAllowOrigin),
|
||||
middlewares.ParseAcl(be),
|
||||
))
|
||||
objectRouter.Put("",
|
||||
@@ -1345,7 +1385,7 @@ func (sa *S3ApiRouter) Init(app *fiber.App, be backend.Backend, iam auth.IAMServ
|
||||
middlewares.VerifyPresignedV4Signature(root, iam, region, true),
|
||||
middlewares.VerifyV4Signature(root, iam, region, true, true),
|
||||
middlewares.VerifyChecksums(true, false, false),
|
||||
middlewares.ApplyBucketCORS(be),
|
||||
middlewares.ApplyBucketCORS(be, corsAllowOrigin),
|
||||
middlewares.ParseAcl(be),
|
||||
))
|
||||
|
||||
@@ -1367,10 +1407,10 @@ func (sa *S3ApiRouter) Init(app *fiber.App, be backend.Backend, iam auth.IAMServ
|
||||
metrics.ActionCopyObject,
|
||||
services,
|
||||
middlewares.BucketObjectNameValidator(),
|
||||
middlewares.ApplyBucketCORS(be, corsAllowOrigin),
|
||||
middlewares.AuthorizePublicBucketAccess(be, metrics.ActionCopyObject, auth.PutObjectAction, auth.PermissionWrite, region, false),
|
||||
middlewares.VerifyPresignedV4Signature(root, iam, region, false),
|
||||
middlewares.VerifyV4Signature(root, iam, region, false, true),
|
||||
middlewares.ApplyBucketCORS(be),
|
||||
middlewares.ParseAcl(be),
|
||||
))
|
||||
objectRouter.Put("",
|
||||
@@ -1379,18 +1419,31 @@ func (sa *S3ApiRouter) Init(app *fiber.App, be backend.Backend, iam auth.IAMServ
|
||||
metrics.ActionPutObject,
|
||||
services,
|
||||
middlewares.BucketObjectNameValidator(),
|
||||
middlewares.ApplyBucketCORS(be, corsAllowOrigin),
|
||||
middlewares.AuthorizePublicBucketAccess(be, metrics.ActionPutObject, auth.PutObjectAction, auth.PermissionWrite, region, true),
|
||||
middlewares.VerifyPresignedV4Signature(root, iam, region, true),
|
||||
middlewares.VerifyV4Signature(root, iam, region, true, true),
|
||||
middlewares.VerifyChecksums(true, false, false),
|
||||
middlewares.ApplyBucketCORS(be),
|
||||
middlewares.ParseAcl(be),
|
||||
))
|
||||
|
||||
app.Options("/:bucket/*", controllers.ProcessHandlers(ctrl.CORSOptions, metrics.ActionOptions, services,
|
||||
middlewares.BucketObjectNameValidator(),
|
||||
middlewares.ParseAcl(be),
|
||||
))
|
||||
app.Options("/:bucket",
|
||||
middlewares.ApplyBucketCORSPreflightFallback(be, corsAllowOrigin),
|
||||
controllers.ProcessHandlers(ctrl.CORSOptions, metrics.ActionOptions, services,
|
||||
middlewares.BucketObjectNameValidator(),
|
||||
middlewares.ApplyBucketCORS(be, corsAllowOrigin),
|
||||
middlewares.ParseAcl(be),
|
||||
),
|
||||
)
|
||||
|
||||
app.Options("/:bucket/*",
|
||||
middlewares.ApplyBucketCORSPreflightFallback(be, corsAllowOrigin),
|
||||
controllers.ProcessHandlers(ctrl.CORSOptions, metrics.ActionOptions, services,
|
||||
middlewares.BucketObjectNameValidator(),
|
||||
middlewares.ApplyBucketCORS(be, corsAllowOrigin),
|
||||
middlewares.ParseAcl(be),
|
||||
),
|
||||
)
|
||||
|
||||
// Return MethodNotAllowed for all the unmatched routes
|
||||
app.All("*", controllers.ProcessHandlers(ctrl.HandleErrorRoute(s3err.GetAPIError(s3err.ErrMethodNotAllowed)), metrics.ActionUndetected, services))
|
||||
|
||||
253
s3api/router_cors_test.go
Normal file
253
s3api/router_cors_test.go
Normal file
@@ -0,0 +1,253 @@
|
||||
// Copyright 2026 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 s3api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/versity/versitygw/auth"
|
||||
"github.com/versity/versitygw/backend"
|
||||
"github.com/versity/versitygw/s3api/middlewares"
|
||||
"github.com/versity/versitygw/s3err"
|
||||
)
|
||||
|
||||
type backendWithCorsOnly struct {
|
||||
backend.BackendUnsupported
|
||||
}
|
||||
|
||||
func (b backendWithCorsOnly) GetBucketCors(ctx context.Context, bucket string) ([]byte, error) {
|
||||
return nil, s3err.GetAPIError(s3err.ErrNoSuchCORSConfiguration)
|
||||
}
|
||||
|
||||
func TestS3ApiRouter_ListBuckets_DefaultCORSAllowOrigin(t *testing.T) {
|
||||
origin := "https://example.com"
|
||||
|
||||
app := fiber.New()
|
||||
(&S3ApiRouter{}).Init(
|
||||
app,
|
||||
backend.BackendUnsupported{},
|
||||
&auth.IAMServiceInternal{},
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
false,
|
||||
"us-east-1",
|
||||
"",
|
||||
middlewares.RootUserConfig{},
|
||||
origin,
|
||||
)
|
||||
|
||||
req, err := http.NewRequest(http.MethodGet, "/", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("new request: %v", err)
|
||||
}
|
||||
|
||||
resp, err := app.Test(req)
|
||||
if err != nil {
|
||||
t.Fatalf("app.Test: %v", err)
|
||||
}
|
||||
|
||||
if got := resp.Header.Get("Access-Control-Allow-Origin"); got != origin {
|
||||
t.Fatalf("expected Access-Control-Allow-Origin %q, got %q", origin, got)
|
||||
}
|
||||
if got := resp.Header.Get("Access-Control-Expose-Headers"); got == "" {
|
||||
t.Fatalf("expected Access-Control-Expose-Headers to be set")
|
||||
}
|
||||
}
|
||||
|
||||
func TestS3ApiRouter_ListBuckets_OptionsPreflight_DefaultCORS(t *testing.T) {
|
||||
origin := "https://example.com"
|
||||
|
||||
app := fiber.New()
|
||||
(&S3ApiRouter{}).Init(
|
||||
app,
|
||||
backend.BackendUnsupported{},
|
||||
&auth.IAMServiceInternal{},
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
false,
|
||||
"us-east-1",
|
||||
"",
|
||||
middlewares.RootUserConfig{},
|
||||
origin,
|
||||
)
|
||||
|
||||
req, err := http.NewRequest(http.MethodOptions, "/", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("new request: %v", err)
|
||||
}
|
||||
req.Header.Set("Origin", "https://client.example")
|
||||
req.Header.Set("Access-Control-Request-Method", "GET")
|
||||
req.Header.Set("Access-Control-Request-Headers", "authorization")
|
||||
|
||||
resp, err := app.Test(req)
|
||||
if err != nil {
|
||||
t.Fatalf("app.Test: %v", err)
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusNoContent {
|
||||
t.Fatalf("expected status %d, got %d", http.StatusNoContent, resp.StatusCode)
|
||||
}
|
||||
if got := resp.Header.Get("Access-Control-Allow-Origin"); got != origin {
|
||||
t.Fatalf("expected Access-Control-Allow-Origin %q, got %q", origin, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestS3ApiRouter_PutBucketTagging_ErrorStillIncludesFallbackCORS(t *testing.T) {
|
||||
origin := "http://127.0.0.1:9090"
|
||||
|
||||
app := fiber.New()
|
||||
(&S3ApiRouter{}).Init(
|
||||
app,
|
||||
backendWithCorsOnly{},
|
||||
&auth.IAMServiceInternal{},
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
false,
|
||||
"us-east-1",
|
||||
"",
|
||||
middlewares.RootUserConfig{},
|
||||
origin,
|
||||
)
|
||||
|
||||
req, err := http.NewRequest(http.MethodPut, "/testing?tagging", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("new request: %v", err)
|
||||
}
|
||||
req.Header.Set("Origin", origin)
|
||||
|
||||
resp, err := app.Test(req)
|
||||
if err != nil {
|
||||
t.Fatalf("app.Test: %v", err)
|
||||
}
|
||||
|
||||
if got := resp.Header.Get("Access-Control-Allow-Origin"); got != origin {
|
||||
t.Fatalf("expected Access-Control-Allow-Origin %q, got %q", origin, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestS3ApiRouter_PutObjectTagging_ErrorStillIncludesFallbackCORS(t *testing.T) {
|
||||
origin := "http://127.0.0.1:9090"
|
||||
|
||||
app := fiber.New()
|
||||
(&S3ApiRouter{}).Init(
|
||||
app,
|
||||
backendWithCorsOnly{},
|
||||
&auth.IAMServiceInternal{},
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
false,
|
||||
"us-east-1",
|
||||
"",
|
||||
middlewares.RootUserConfig{},
|
||||
origin,
|
||||
)
|
||||
|
||||
req, err := http.NewRequest(http.MethodPut, "/testing/myobj?tagging", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("new request: %v", err)
|
||||
}
|
||||
req.Header.Set("Origin", origin)
|
||||
|
||||
resp, err := app.Test(req)
|
||||
if err != nil {
|
||||
t.Fatalf("app.Test: %v", err)
|
||||
}
|
||||
|
||||
if got := resp.Header.Get("Access-Control-Allow-Origin"); got != origin {
|
||||
t.Fatalf("expected Access-Control-Allow-Origin %q, got %q", origin, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestS3ApiRouter_CopyObject_ErrorStillIncludesFallbackCORS(t *testing.T) {
|
||||
origin := "http://127.0.0.1:9090"
|
||||
|
||||
app := fiber.New()
|
||||
(&S3ApiRouter{}).Init(
|
||||
app,
|
||||
backendWithCorsOnly{},
|
||||
&auth.IAMServiceInternal{},
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
false,
|
||||
"us-east-1",
|
||||
"",
|
||||
middlewares.RootUserConfig{},
|
||||
origin,
|
||||
)
|
||||
|
||||
req, err := http.NewRequest(http.MethodPut, "/testing/myobj", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("new request: %v", err)
|
||||
}
|
||||
req.Header.Set("Origin", origin)
|
||||
req.Header.Set("X-Amz-Copy-Source", "srcbucket/srckey")
|
||||
|
||||
resp, err := app.Test(req)
|
||||
if err != nil {
|
||||
t.Fatalf("app.Test: %v", err)
|
||||
}
|
||||
|
||||
if got := resp.Header.Get("Access-Control-Allow-Origin"); got != origin {
|
||||
t.Fatalf("expected Access-Control-Allow-Origin %q, got %q", origin, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestS3ApiRouter_PutObject_ErrorStillIncludesFallbackCORS(t *testing.T) {
|
||||
origin := "http://127.0.0.1:9090"
|
||||
|
||||
app := fiber.New()
|
||||
(&S3ApiRouter{}).Init(
|
||||
app,
|
||||
backendWithCorsOnly{},
|
||||
&auth.IAMServiceInternal{},
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
false,
|
||||
"us-east-1",
|
||||
"",
|
||||
middlewares.RootUserConfig{},
|
||||
origin,
|
||||
)
|
||||
|
||||
req, err := http.NewRequest(http.MethodPut, "/testing/myobj", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("new request: %v", err)
|
||||
}
|
||||
req.Header.Set("Origin", origin)
|
||||
|
||||
resp, err := app.Test(req)
|
||||
if err != nil {
|
||||
t.Fatalf("app.Test: %v", err)
|
||||
}
|
||||
|
||||
if got := resp.Header.Get("Access-Control-Allow-Origin"); got != origin {
|
||||
t.Fatalf("expected Access-Control-Allow-Origin %q, got %q", origin, got)
|
||||
}
|
||||
}
|
||||
@@ -46,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, "us-east-1", "", middlewares.RootUserConfig{})
|
||||
tt.sa.Init(tt.args.app, tt.args.be, tt.args.iam, nil, nil, nil, nil, false, "us-east-1", "", middlewares.RootUserConfig{}, "")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,16 +41,17 @@ const (
|
||||
)
|
||||
|
||||
type S3ApiServer struct {
|
||||
app *fiber.App
|
||||
backend backend.Backend
|
||||
router *S3ApiRouter
|
||||
port string
|
||||
cert *tls.Certificate
|
||||
quiet bool
|
||||
readonly bool
|
||||
keepAlive bool
|
||||
health string
|
||||
virtualDomain string
|
||||
app *fiber.App
|
||||
backend backend.Backend
|
||||
router *S3ApiRouter
|
||||
port string
|
||||
cert *tls.Certificate
|
||||
quiet bool
|
||||
readonly bool
|
||||
keepAlive bool
|
||||
health string
|
||||
virtualDomain string
|
||||
corsAllowOrigin string
|
||||
}
|
||||
|
||||
func New(
|
||||
@@ -123,7 +124,7 @@ func New(
|
||||
app.Use(middlewares.DebugLogger())
|
||||
}
|
||||
|
||||
server.router.Init(app, be, iam, l, adminLogger, evs, mm, server.readonly, region, server.virtualDomain, root)
|
||||
server.router.Init(app, be, iam, l, adminLogger, evs, mm, server.readonly, region, server.virtualDomain, root, server.corsAllowOrigin)
|
||||
|
||||
return server, nil
|
||||
}
|
||||
@@ -165,6 +166,12 @@ func WithKeepAlive() Option {
|
||||
return func(s *S3ApiServer) { s.keepAlive = true }
|
||||
}
|
||||
|
||||
// WithCORSAllowOrigin sets the default CORS Access-Control-Allow-Origin value.
|
||||
// This is applied when no bucket CORS configuration exists, and for admin APIs.
|
||||
func WithCORSAllowOrigin(origin string) Option {
|
||||
return func(s *S3ApiServer) { s.corsAllowOrigin = origin }
|
||||
}
|
||||
|
||||
func (sa *S3ApiServer) Serve() (err error) {
|
||||
if sa.cert != nil {
|
||||
return sa.app.ListenTLSWithCertificate(sa.port, *sa.cert)
|
||||
|
||||
@@ -222,13 +222,6 @@ func PreflightOPTIONS_access_granted(s *S3Conf) error {
|
||||
ExposeHeaders: []string{"X-Amz-Expected-Bucket-Owner"},
|
||||
MaxAgeSeconds: getPtr(int32(5000)),
|
||||
},
|
||||
{
|
||||
AllowedOrigins: []string{"http://uniquie-origin.net"},
|
||||
AllowedMethods: []string{http.MethodPost, http.MethodPut},
|
||||
AllowedHeaders: []string{"X-Amz-*-Suffix"},
|
||||
ExposeHeaders: []string{"Authorization", "Content-Type"},
|
||||
MaxAgeSeconds: getPtr(int32(2000)),
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -245,22 +238,19 @@ func PreflightOPTIONS_access_granted(s *S3Conf) error {
|
||||
result PreflightResult
|
||||
}{
|
||||
// first rule matches
|
||||
{"http://example.com", http.MethodGet, "X-Amz-Date", PreflightResult{"http://example.com", "GET, HEAD", "x-amz-date", "Content-Type, Content-Length", "100", "true", varyHdr, nil}},
|
||||
{"http://example.com", http.MethodGet, "X-Amz-Content-Sha256", PreflightResult{"http://example.com", "GET, HEAD", "x-amz-content-sha256", "Content-Type, Content-Length", "100", "true", varyHdr, nil}},
|
||||
{"http://example.com", http.MethodHead, "", PreflightResult{"http://example.com", "GET, HEAD", "", "Content-Type, Content-Length", "100", "true", varyHdr, nil}},
|
||||
{"https://example.com", http.MethodGet, "X-Amz-Date,X-Amz-Content-Sha256", PreflightResult{"https://example.com", "GET, HEAD", "x-amz-date, x-amz-content-sha256", "Content-Type, Content-Length", "100", "true", varyHdr, nil}},
|
||||
{"http://example.com", http.MethodGet, "X-Amz-Date", PreflightResult{"http://example.com", "GET, HEAD", "x-amz-date", "Content-Type, Content-Length, ETag", "100", "true", varyHdr, nil}},
|
||||
{"http://example.com", http.MethodGet, "X-Amz-Content-Sha256", PreflightResult{"http://example.com", "GET, HEAD", "x-amz-content-sha256", "Content-Type, Content-Length, ETag", "100", "true", varyHdr, nil}},
|
||||
{"http://example.com", http.MethodHead, "", PreflightResult{"http://example.com", "GET, HEAD", "", "Content-Type, Content-Length, ETag", "100", "true", varyHdr, nil}},
|
||||
{"https://example.com", http.MethodGet, "X-Amz-Date,X-Amz-Content-Sha256", PreflightResult{"https://example.com", "GET, HEAD", "x-amz-date, x-amz-content-sha256", "Content-Type, Content-Length, ETag", "100", "true", varyHdr, nil}},
|
||||
// second rule matches: origin is a wildcard
|
||||
{"http://anything.com", http.MethodHead, "X-Amz-Meta-Something", PreflightResult{"*", "HEAD", "x-amz-meta-something", "", "", "false", varyHdr, nil}},
|
||||
{"hello.com", http.MethodHead, "", PreflightResult{"*", "HEAD", "", "", "", "false", varyHdr, nil}},
|
||||
{"http://anything.com", http.MethodHead, "X-Amz-Meta-Something", PreflightResult{"*", "HEAD", "x-amz-meta-something", "ETag", "", "false", varyHdr, nil}},
|
||||
{"hello.com", http.MethodHead, "", PreflightResult{"*", "HEAD", "", "ETag", "", "false", varyHdr, nil}},
|
||||
// third rule matches
|
||||
{"something.net", http.MethodPut, "Authorization", PreflightResult{"something.net", "POST, PUT", "authorization", "Content-Disposition, Content-Encoding", "3000", "true", varyHdr, nil}},
|
||||
{"something.net", http.MethodPost, "", PreflightResult{"something.net", "POST, PUT", "", "Content-Disposition, Content-Encoding", "3000", "true", varyHdr, nil}},
|
||||
{"something.net", http.MethodPut, "Authorization", PreflightResult{"something.net", "POST, PUT", "authorization", "Content-Disposition, Content-Encoding, ETag", "3000", "true", varyHdr, nil}},
|
||||
{"something.net", http.MethodPost, "", PreflightResult{"something.net", "POST, PUT", "", "Content-Disposition, Content-Encoding, ETag", "3000", "true", varyHdr, nil}},
|
||||
// forth rule matches: origin contains wildcard
|
||||
{"http://www.hello.world.com", http.MethodGet, "", PreflightResult{"http://www.hello.world.com", "GET", "", "X-Amz-Expected-Bucket-Owner", "5000", "true", varyHdr, nil}},
|
||||
{"http://www.example.com", http.MethodGet, "x-amz-server-side-encryption", PreflightResult{"http://www.example.com", "GET", "x-amz-server-side-encryption", "X-Amz-Expected-Bucket-Owner", "5000", "true", varyHdr, nil}},
|
||||
// fifth rule matches: allowed headers contains wildcard
|
||||
{"http://uniquie-origin.net", http.MethodPost, "X-Amz-anything-Suffix", PreflightResult{"http://uniquie-origin.net", "POST, PUT", "x-amz-anything-suffix", "Authorization, Content-Type", "2000", "true", varyHdr, nil}},
|
||||
{"http://uniquie-origin.net", http.MethodPut, "X-Amz-yyy-xxx-Suffix", PreflightResult{"http://uniquie-origin.net", "POST, PUT", "x-amz-yyy-xxx-suffix", "Authorization, Content-Type", "2000", "true", varyHdr, nil}},
|
||||
{"http://www.hello.world.com", http.MethodGet, "", PreflightResult{"http://www.hello.world.com", "GET", "", "X-Amz-Expected-Bucket-Owner, ETag", "5000", "true", varyHdr, nil}},
|
||||
{"http://www.example.com", http.MethodGet, "x-amz-server-side-encryption", PreflightResult{"http://www.example.com", "GET", "x-amz-server-side-encryption", "X-Amz-Expected-Bucket-Owner, ETag", "5000", "true", varyHdr, nil}},
|
||||
} {
|
||||
err := testOPTIONSEdnpoint(s, bucket, test.origin, test.method, test.headers, &test.result)
|
||||
if err != nil {
|
||||
|
||||
Reference in New Issue
Block a user