Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions buildscripts/rebrand-guard/compat-baseline.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
"github.com/minio/minio/internal/auth",
"github.com/minio/minio/internal/bpool",
"github.com/minio/minio/internal/bucket/bandwidth",
"github.com/minio/minio/internal/bucket/cors",
"github.com/minio/minio/internal/bucket/encryption",
"github.com/minio/minio/internal/bucket/lifecycle",
"github.com/minio/minio/internal/bucket/object/lock",
Expand Down Expand Up @@ -857,6 +858,7 @@
"/minio/health/cluster/read",
"/minio/health/live",
"/minio/health/ready",
"/mybucket/obj",
"/myobject*",
"/netperf",
"/newfolder",
Expand Down Expand Up @@ -1708,6 +1710,8 @@
"cmd:cmd:field:BucketMetadata.BucketTargetsConfigMetaJSON",
"cmd:cmd:field:BucketMetadata.BucketTargetsConfigMetaUpdatedAt",
"cmd:cmd:field:BucketMetadata.BucketTargetsConfigUpdatedAt",
"cmd:cmd:field:BucketMetadata.CorsConfigUpdatedAt",
"cmd:cmd:field:BucketMetadata.CorsConfigXML",
"cmd:cmd:field:BucketMetadata.Created",
"cmd:cmd:field:BucketMetadata.EncryptionConfigUpdatedAt",
"cmd:cmd:field:BucketMetadata.EncryptionConfigXML",
Expand Down Expand Up @@ -3126,6 +3130,8 @@
"cmd:cmd:method:BucketMetadataSys.GetBucketTargetsConfig",
"cmd:cmd:method:BucketMetadataSys.GetConfig",
"cmd:cmd:method:BucketMetadataSys.GetConfigFromDisk",
"cmd:cmd:method:BucketMetadataSys.GetCorsConfig",
"cmd:cmd:method:BucketMetadataSys.GetCorsConfigXML",
"cmd:cmd:method:BucketMetadataSys.GetLifecycleConfig",
"cmd:cmd:method:BucketMetadataSys.GetNotificationConfig",
"cmd:cmd:method:BucketMetadataSys.GetObjectLockConfig",
Expand Down Expand Up @@ -5872,6 +5878,23 @@
"internal/bucket/bandwidth:bandwidth:type:MonitorReaderOptions",
"internal/bucket/bandwidth:bandwidth:type:MonitoredReader",
"internal/bucket/bandwidth:bandwidth:type:SelectionFunction",
"internal/bucket/cors:cors:field:Config.CORSRules",
"internal/bucket/cors:cors:field:Config.XMLName",
"internal/bucket/cors:cors:field:Rule.AllowedHeaders",
"internal/bucket/cors:cors:field:Rule.AllowedMethods",
"internal/bucket/cors:cors:field:Rule.AllowedOrigins",
"internal/bucket/cors:cors:field:Rule.ExposeHeaders",
"internal/bucket/cors:cors:field:Rule.ID",
"internal/bucket/cors:cors:field:Rule.MaxAgeSeconds",
"internal/bucket/cors:cors:func:ParseBucketCorsConfig",
"internal/bucket/cors:cors:method:Config.MatchPreflight",
"internal/bucket/cors:cors:method:Config.MatchRule",
"internal/bucket/cors:cors:method:Config.Validate",
"internal/bucket/cors:cors:method:Rule.FilterAllowedHeaders",
"internal/bucket/cors:cors:method:Rule.HasAllowedMethod",
"internal/bucket/cors:cors:method:Rule.HasAllowedOrigin",
"internal/bucket/cors:cors:type:Config",
"internal/bucket/cors:cors:type:Rule",
"internal/bucket/encryption:sse:const:AES256",
"internal/bucket/encryption:sse:const:AWSKms",
"internal/bucket/encryption:sse:field:ApplyOptions.AutoEncrypt",
Expand Down
90 changes: 84 additions & 6 deletions cmd/api-router.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,11 @@ package cmd
import (
"net"
"net/http"
"strconv"
"strings"

consoleapi "github.com/minio/console/api"
bktcors "github.com/minio/minio/internal/bucket/cors"
xhttp "github.com/minio/minio/internal/http"
"github.com/minio/mux"
"github.com/minio/pkg/v3/wildcard"
Expand Down Expand Up @@ -111,11 +114,6 @@ var rejectedBucketAPIs = []rejectedAPI{
methods: []string{http.MethodGet, http.MethodPut, http.MethodDelete},
queries: []string{"inventory", ""},
},
{
api: "cors",
methods: []string{http.MethodPut, http.MethodDelete},
queries: []string{"cors", ""},
},
{
api: "metrics",
methods: []string{http.MethodGet, http.MethodPut, http.MethodDelete},
Expand Down Expand Up @@ -648,6 +646,74 @@ func registerAPIRouter(router *mux.Router) {
apiRouter.MethodNotAllowedHandler = collectAPIStats("methodnotallowed", httpTraceAll(methodNotAllowedHandler("S3")))
}

// applyBucketCors applies a bucket's CORS configuration to the request.
// For an OPTIONS preflight it writes the full CORS response and returns true
// (request is complete). For an actual request it adds the applicable
// Access-Control-* response headers and returns false so the request
// continues down the handler chain. If no rule matches a preflight it writes
// 403 and returns true.
func applyBucketCors(w http.ResponseWriter, r *http.Request, cfg *bktcors.Config) (handled bool) {
origin := r.Header.Get("Origin")
if origin == "" {
return false // not a CORS request
}

isPreflight := r.Method == http.MethodOptions &&
r.Header.Get("Access-Control-Request-Method") != ""

if isPreflight {
method := r.Header.Get("Access-Control-Request-Method")
reqHeaders := splitAndTrim(r.Header.Get("Access-Control-Request-Headers"))
rule, allowedHeaders, ok := cfg.MatchPreflight(origin, method, reqHeaders)
if !ok {
writeResponse(w, http.StatusForbidden, nil, mimeNone)
return true
}
h := w.Header()
h.Set("Access-Control-Allow-Origin", origin)
h.Set("Access-Control-Allow-Methods", method)
if len(allowedHeaders) > 0 {
h.Set("Access-Control-Allow-Headers", strings.Join(allowedHeaders, ", "))
}
if rule.MaxAgeSeconds > 0 {
h.Set("Access-Control-Max-Age", strconv.Itoa(rule.MaxAgeSeconds))
}
h.Set("Access-Control-Allow-Credentials", "true")
h.Add("Vary", "Origin")
writeResponse(w, http.StatusOK, nil, mimeNone)
return true
}

// Actual request: attach headers if the origin+method match.
rule, ok := cfg.MatchRule(origin, r.Method)
if !ok {
return false // no matching rule → no CORS headers, continue normally
}
h := w.Header()
h.Set("Access-Control-Allow-Origin", origin)
h.Set("Access-Control-Allow-Credentials", "true")
if len(rule.ExposeHeaders) > 0 {
h.Set("Access-Control-Expose-Headers", strings.Join(rule.ExposeHeaders, ", "))
}
h.Add("Vary", "Origin")
return false
}

// splitAndTrim splits a comma-separated header list into trimmed, non-empty values.
func splitAndTrim(s string) []string {
if s == "" {
return nil
}
parts := strings.Split(s, ",")
out := parts[:0]
for _, p := range parts {
if p = strings.TrimSpace(p); p != "" {
out = append(out, p)
}
}
return out
}

// corsHandler handler for CORS (Cross Origin Resource Sharing)
func corsHandler(handler http.Handler) http.Handler {
commonS3Headers := []string{
Expand Down Expand Up @@ -693,5 +759,17 @@ func corsHandler(handler http.Handler) http.Handler {
ExposedHeaders: commonS3Headers,
AllowCredentials: true,
}
return cors.New(opts).Handler(handler)
globalCors := cors.New(opts).Handler(handler)
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if bucket, _ := request2BucketObjectName(r); bucket != "" && globalBucketMetadataSys != nil {
if cfg, _, err := globalBucketMetadataSys.GetCorsConfig(bucket); err == nil && cfg != nil {
if applyBucketCors(w, r, cfg) {
return
}
handler.ServeHTTP(w, r)
return
}
}
globalCors.ServeHTTP(w, r)
})
}
190 changes: 190 additions & 0 deletions cmd/bucket-cors-handlers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
// Copyright (c) 2015-2021 MinIO, Inc.
//
// This file is part of MinIO Object Storage stack
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.

package cmd

import (
"bytes"
"encoding/base64"
"errors"
"io"
"net/http"

humanize "github.com/dustin/go-humanize"
"github.com/minio/madmin-go/v3"
"github.com/minio/minio/internal/bucket/cors"
"github.com/minio/minio/internal/logger"
"github.com/minio/mux"
"github.com/minio/pkg/v3/policy"
)

// maxBucketCorsSize is the maximum allowed size of a CORS configuration document.
const maxBucketCorsSize = 64 * humanize.KiByte

// PutBucketCorsHandler - PUT bucket cors.
func (api objectAPIHandlers) PutBucketCorsHandler(w http.ResponseWriter, r *http.Request) {
ctx := newContext(r, w, "PutBucketCors")

defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))

objAPI := api.ObjectAPI()
if objAPI == nil {
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
return
}

vars := mux.Vars(r)
bucket := vars["bucket"]

if s3Error := checkRequestAuthType(ctx, r, policy.PutBucketCorsAction, bucket, ""); s3Error != ErrNone {
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL)
return
}

if _, err := objAPI.GetBucketInfo(ctx, bucket, BucketOptions{}); err != nil {
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
return
}

if r.ContentLength <= 0 {
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrMissingContentLength), r.URL)
return
}
if r.ContentLength > maxBucketCorsSize {
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrEntityTooLarge), r.URL)
return
}

corsBytes, err := io.ReadAll(io.LimitReader(r.Body, r.ContentLength))
if err != nil {
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
return
}

corsCfg, err := cors.ParseBucketCorsConfig(bytes.NewReader(corsBytes))
if err != nil {
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrMalformedXML), r.URL)
return
}
if err := corsCfg.Validate(); err != nil {
writeErrorResponse(ctx, w, APIError{
Code: "MalformedXML",
HTTPStatusCode: http.StatusBadRequest,
Description: err.Error(),
}, r.URL)
return
}

updatedAt, err := globalBucketMetadataSys.Update(ctx, bucket, bucketCorsConfig, corsBytes)
if err != nil {
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
return
}

// Call site replication hook.
//
// We encode the xml bytes as base64 to ensure there are no encoding
// errors.
cfgStr := base64.StdEncoding.EncodeToString(corsBytes)
replLogIf(ctx, globalSiteReplicationSys.BucketMetaHook(ctx, madmin.SRBucketMeta{
Type: madmin.SRBucketMetaTypeCorsConfig,
Bucket: bucket,
Cors: &cfgStr,
UpdatedAt: updatedAt,
}))

writeSuccessResponseHeadersOnly(w)
}

// GetBucketCorsHandler - GET bucket cors.
func (api objectAPIHandlers) GetBucketCorsHandler(w http.ResponseWriter, r *http.Request) {
ctx := newContext(r, w, "GetBucketCors")

defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))

objAPI := api.ObjectAPI()
if objAPI == nil {
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
return
}

vars := mux.Vars(r)
bucket := vars["bucket"]

if s3Error := checkRequestAuthType(ctx, r, policy.GetBucketCorsAction, bucket, ""); s3Error != ErrNone {
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL)
return
}

if _, err := objAPI.GetBucketInfo(ctx, bucket, BucketOptions{}); err != nil {
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
return
}

configData, _, err := globalBucketMetadataSys.GetCorsConfigXML(bucket)
if err != nil {
if errors.Is(err, errConfigNotFound) {
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrNoSuchCORSConfiguration), r.URL)
return
}
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
return
}

writeSuccessResponseXML(w, configData)
}

// DeleteBucketCorsHandler - DELETE bucket cors.
func (api objectAPIHandlers) DeleteBucketCorsHandler(w http.ResponseWriter, r *http.Request) {
ctx := newContext(r, w, "DeleteBucketCors")

defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))

objAPI := api.ObjectAPI()
if objAPI == nil {
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
return
}

vars := mux.Vars(r)
bucket := vars["bucket"]

if s3Error := checkRequestAuthType(ctx, r, policy.DeleteBucketCorsAction, bucket, ""); s3Error != ErrNone {
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL)
return
}

if _, err := objAPI.GetBucketInfo(ctx, bucket, BucketOptions{}); err != nil {
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
return
}

updatedAt, err := globalBucketMetadataSys.Delete(ctx, bucket, bucketCorsConfig)
if err != nil {
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
return
}

replLogIf(ctx, globalSiteReplicationSys.BucketMetaHook(ctx, madmin.SRBucketMeta{
Type: madmin.SRBucketMetaTypeCorsConfig,
Bucket: bucket,
Cors: nil,
UpdatedAt: updatedAt,
}))

writeSuccessNoContent(w)
}
Loading