mirror of
https://github.com/pgsty/minio.git
synced 2026-09-05 18:16:16 +03:00
Merge branch 'codex/prerelease-cleanup': pre-release cleanup and dependency pins
Fifteen reviewed commits: streaming-trailer trust fix and a single replication-trust evaluator, Object Lock versioning invariant, resident-only CORS lookup with fail-closed startup and load-failure states, dead-code removal, compatibility baseline without exported symbols, upstream minio-go with Console 43f8447fd and mcli 20260901, advisory ledger SN-2026-006..010, contributor records, verification scripts with a valid root password, and a deterministic dynamic-timeout test. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PvgysXDmhPBBimCReYtA8q Signed-off-by: Feng Ruohang <rh@vonng.com>
This commit is contained in:
+1
-4
@@ -5,7 +5,7 @@ linters:
|
||||
- durationcheck
|
||||
- forcetypeassert
|
||||
- gocritic
|
||||
- gomodguard
|
||||
- gomodguard_v2
|
||||
- govet
|
||||
- ineffassign
|
||||
- misspell
|
||||
@@ -59,9 +59,6 @@ formatters:
|
||||
exclusions:
|
||||
generated: lax
|
||||
paths:
|
||||
# gofumpt v0.11.0 and Go 1.27's gofmt disagree on the indentation of
|
||||
# multiple composite literals returned from a single statement.
|
||||
- internal/ioutil/wait_pipe\.go$
|
||||
- third_party$
|
||||
- builtin$
|
||||
- examples$
|
||||
|
||||
@@ -44,6 +44,10 @@ Before opening a pull request:
|
||||
|
||||
- Add or update tests for changed behavior.
|
||||
- Run `make verifiers`.
|
||||
- If `make rebrand-guard` reports a changed compatibility set, review the
|
||||
listed identifiers; when the change is intended, refresh the baseline with
|
||||
`go run ./buildscripts/rebrand-guard --write` and commit
|
||||
`buildscripts/rebrand-guard/compat-baseline.json`.
|
||||
- Run the smallest relevant package tests, then `make test` when practical.
|
||||
- Run `make build` and confirm the generated executable is `silo`.
|
||||
- Explain any preserved `MINIO_*`, `minio_*`, `x-minio-*`, `/minio/*`,
|
||||
|
||||
+3
-1
@@ -23,6 +23,9 @@ Contributors whose changes are merged into `main`.
|
||||
| [@mfredenhagen](https://github.com/mfredenhagen) | Bumped `go.opentelemetry.io` to address CVE-2026-39883 | [#19](https://github.com/pgsty/silo/pull/19) | [`1869bd3`](https://github.com/pgsty/silo/commit/1869bd30b) |
|
||||
| [@pinginfo](https://github.com/pinginfo) | Implemented `Flush` on `trackingResponseWriter`, repairing bucket notification streaming | [#34](https://github.com/pgsty/silo/pull/34) | [`65795ee`](https://github.com/pgsty/silo/commit/65795ee1f) |
|
||||
| [@waterkip](https://github.com/waterkip) | Repointed documentation links from the upstream domain to the Silo portal | [#41](https://github.com/pgsty/silo/pull/41) | [`d495d30`](https://github.com/pgsty/silo/commit/d495d30d5) |
|
||||
| [@Dansyuqri](https://github.com/Dansyuqri) | Added `ChecksumType` to the `CompleteMultipartUpload` response | [#57](https://github.com/pgsty/silo/pull/57) | [`d014a12`](https://github.com/pgsty/silo/commit/d014a12cf) |
|
||||
| [@ycjlin](https://github.com/ycjlin) | `ListObjects` returns `NoSuchBucket` for a prefix on a missing bucket | [#37](https://github.com/pgsty/silo/pull/37) | [`e9c5340`](https://github.com/pgsty/silo/commit/e9c5340be) |
|
||||
| [@h5vx](https://github.com/h5vx) | Implemented per-bucket CORS: stored configuration, S3 handlers, and request enforcement | [#71](https://github.com/pgsty/silo/pull/71) | [`e4e3007`](https://github.com/pgsty/silo/commit/e4e3007da) |
|
||||
|
||||
## Proposed changes
|
||||
|
||||
@@ -32,7 +35,6 @@ differently.
|
||||
| Contributor | Change | Pull request | Status |
|
||||
| :-- | :-- | :-- | :-- |
|
||||
| [@magicxor](https://github.com/magicxor) | `DELETE` precondition checks for the `If-Match` header | [#12](https://github.com/pgsty/silo/pull/12) | Open, queued for review |
|
||||
| [@ycjlin](https://github.com/ycjlin) | `ListObjects` should return `NoSuchBucket` for a prefix on a missing bucket | [#37](https://github.com/pgsty/silo/pull/37) | Open, queued for review |
|
||||
| [@davinkevin](https://github.com/davinkevin) | Distroless-based Docker image variant | [#21](https://github.com/pgsty/silo/pull/21) | Superseded by the distroless variant shipped in RELEASE.2026-08-06, which the PR anticipated by four months |
|
||||
| [@lem21h](https://github.com/lem21h) | Assorted fixes and improvements | [#36](https://github.com/pgsty/silo/pull/36) | Closed |
|
||||
| [@sulin37392](https://github.com/sulin37392) | Dependency updates against the fork | [#8](https://github.com/pgsty/silo/pull/8) | Closed |
|
||||
|
||||
@@ -17599,20 +17599,67 @@ For more information on this, and how to apply and follow the GNU AGPL, see
|
||||
|
||||
Bundled NOTICE file:
|
||||
|
||||
This file is part of Console Server
|
||||
SILO Console
|
||||
============
|
||||
|
||||
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 product includes software developed at MinIO, Inc. (https://min.io/):
|
||||
MinIO Console, Copyright (c) 2015-2026 MinIO, Inc.
|
||||
|
||||
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.
|
||||
SILO Console (this distribution, published from https://github.com/pgsty/silo-console
|
||||
and shipped as `silo-console`) is a community-maintained fork of MinIO Console.
|
||||
The code was carried forward through two earlier community maintenance lines
|
||||
before this one:
|
||||
|
||||
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/>.
|
||||
- Alevsk/console https://github.com/Alevsk/console
|
||||
- georgmangold/console https://github.com/georgmangold/console
|
||||
Console portions Copyright (c) Georg Mangold and contributors
|
||||
|
||||
Copyright in inherited code remains with MinIO, Inc. and the respective
|
||||
contributors. Modifications authored for SILO by PGSTY are
|
||||
Copyright (c) 2025-2026 PGSTY (Ruohang Feng) and the SILO contributors; other
|
||||
modifications remain the copyright of their respective authors. All existing
|
||||
copyright, license and attribution notices are kept intact.
|
||||
|
||||
SILO and SILO Console are independent community projects and are not
|
||||
affiliated with, endorsed by, or sponsored by MinIO, Inc. MinIO(R) is a
|
||||
registered trademark of MinIO, Inc. Amazon S3 is a trademark of Amazon.com,
|
||||
Inc. or its affiliates; references to S3 describe protocol compatibility only.
|
||||
|
||||
License
|
||||
-------
|
||||
|
||||
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 (the LICENSE file next to this notice). If not, see
|
||||
<https://www.gnu.org/licenses/>.
|
||||
|
||||
Corresponding source
|
||||
--------------------
|
||||
|
||||
Under section 13 of the AGPL, every user who interacts with this program over a
|
||||
network is offered its corresponding source. A release build reports the exact
|
||||
source revision it was built from: `console version` prints it, the HTTP server
|
||||
serves it in the page metadata used by the License, Login and anonymous pages,
|
||||
and the container image carries it in the `io.pgsty.silo-console.source`
|
||||
label. A build that cannot prove its exact revision says so instead of
|
||||
guessing; operators of such builds must publish their modified source and may
|
||||
point users at it with CONSOLE_CORRESPONDING_SOURCE_URL.
|
||||
|
||||
Third-party notices
|
||||
-------------------
|
||||
|
||||
The licenses and notices of every third-party component linked into the binary
|
||||
or bundled into the web application are collected in the CREDITS file next to
|
||||
this notice (`console credits`, or /legal/CREDITS on a running server).
|
||||
|
||||
================================================================
|
||||
|
||||
@@ -21707,8 +21754,8 @@ https://github.com/minio/md5-simd
|
||||
|
||||
================================================================
|
||||
|
||||
github.com/minio/minio-go/v7 (replaced by github.com/pgsty/silo-go/v7)
|
||||
https://github.com/pgsty/silo-go/v7
|
||||
github.com/minio/minio-go/v7
|
||||
https://github.com/minio/minio-go/v7
|
||||
----------------------------------------------------------------
|
||||
|
||||
Apache License
|
||||
|
||||
@@ -6,9 +6,9 @@ ENV GOPATH=/go
|
||||
ENV CGO_ENABLED=0
|
||||
|
||||
ARG MC_REPO=pgsty/mc
|
||||
ARG MC_VERSION=RELEASE.2026-08-06T00-00-00Z
|
||||
ARG MC_AMD64_SHA256=4b488bd30af54ad4214e5b654746677c79cd93dc6cad4be3aa2d09dbb48370ff
|
||||
ARG MC_ARM64_SHA256=83f6fedb16ed9c1e8efa8aea6776203dff132bc474214543d5c0767ed2066c2f
|
||||
ARG MC_VERSION=RELEASE.2026-09-01T00-00-00Z
|
||||
ARG MC_AMD64_SHA256=6387cbeebb17c4bd52b447ee332ba22777e129034befa6598308c3aa04f02d09
|
||||
ARG MC_ARM64_SHA256=5fc434c7e416bb4787e92306a8165d55284aac639a29744160b2a198fdd7573a
|
||||
|
||||
RUN apk add -U --no-cache \
|
||||
ca-certificates \
|
||||
|
||||
@@ -114,6 +114,8 @@ Report vulnerabilities privately as described in [`SECURITY.md`](SECURITY.md); e
|
||||
<p>
|
||||
<a href="https://github.com/magicxor"><img src="https://github.com/magicxor.png?size=64" width="44" alt="magicxor" title="@magicxor"></a>
|
||||
<a href="https://github.com/ycjlin"><img src="https://github.com/ycjlin.png?size=64" width="44" alt="ycjlin" title="@ycjlin"></a>
|
||||
<a href="https://github.com/h5vx"><img src="https://github.com/h5vx.png?size=64" width="44" alt="h5vx" title="@h5vx"></a>
|
||||
<a href="https://github.com/Dansyuqri"><img src="https://github.com/Dansyuqri.png?size=64" width="44" alt="Dansyuqri" title="@Dansyuqri"></a>
|
||||
<a href="https://github.com/davinkevin"><img src="https://github.com/davinkevin.png?size=64" width="44" alt="davinkevin" title="@davinkevin"></a>
|
||||
<a href="https://github.com/lem21h"><img src="https://github.com/lem21h.png?size=64" width="44" alt="lem21h" title="@lem21h"></a>
|
||||
<a href="https://github.com/sulin37392"><img src="https://github.com/sulin37392.png?size=64" width="44" alt="sulin37392" title="@sulin37392"></a>
|
||||
|
||||
@@ -114,6 +114,8 @@ S3 API、`MINIO_*` 环境变量、`minio_*` 指标、`x-minio-*` 头、`/minio/*
|
||||
<p>
|
||||
<a href="https://github.com/magicxor"><img src="https://github.com/magicxor.png?size=64" width="44" alt="magicxor" title="@magicxor"></a>
|
||||
<a href="https://github.com/ycjlin"><img src="https://github.com/ycjlin.png?size=64" width="44" alt="ycjlin" title="@ycjlin"></a>
|
||||
<a href="https://github.com/h5vx"><img src="https://github.com/h5vx.png?size=64" width="44" alt="h5vx" title="@h5vx"></a>
|
||||
<a href="https://github.com/Dansyuqri"><img src="https://github.com/Dansyuqri.png?size=64" width="44" alt="Dansyuqri" title="@Dansyuqri"></a>
|
||||
<a href="https://github.com/davinkevin"><img src="https://github.com/davinkevin.png?size=64" width="44" alt="davinkevin" title="@davinkevin"></a>
|
||||
<a href="https://github.com/lem21h"><img src="https://github.com/lem21h.png?size=64" width="44" alt="lem21h" title="@lem21h"></a>
|
||||
<a href="https://github.com/sulin37392"><img src="https://github.com/sulin37392.png?size=64" width="44" alt="sulin37392" title="@sulin37392"></a>
|
||||
|
||||
@@ -22,8 +22,8 @@ function start_silo_4drive() {
|
||||
start_port=$1
|
||||
|
||||
export MINIO_ROOT_USER=silo
|
||||
export MINIO_ROOT_PASSWORD=silo123
|
||||
export MC_HOST_silo="http://silo:silo123@127.0.0.1:${start_port}/"
|
||||
export MINIO_ROOT_PASSWORD=silo1234
|
||||
export MC_HOST_silo="http://silo:silo1234@127.0.0.1:${start_port}/"
|
||||
unset MINIO_KMS_AUTO_ENCRYPTION # do not auto-encrypt objects
|
||||
export MINIO_CI_CD=1
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@ if [ -n "${MCLI_BIN:-}" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
release=${MCLI_RELEASE:-RELEASE.2026-08-06T00-00-00Z}
|
||||
release=${MCLI_RELEASE:-RELEASE.2026-09-01T00-00-00Z}
|
||||
version_hyphen=${release#RELEASE.}
|
||||
package_version=$(printf '%s\n' "${version_hyphen}" | sed -E 's/^([0-9]{4})-([0-9]{2})-([0-9]{2})T([0-9]{2})-([0-9]{2})-([0-9]{2})Z$/\1\2\3\4\5\6.0.0/')
|
||||
if [ "${package_version}" = "${version_hyphen}" ]; then
|
||||
|
||||
@@ -45,8 +45,8 @@ function start_silo_10drive() {
|
||||
start_port=$1
|
||||
|
||||
export MINIO_ROOT_USER=silo
|
||||
export MINIO_ROOT_PASSWORD=silo123
|
||||
export MC_HOST_silo="http://silo:silo123@127.0.0.1:${start_port}/"
|
||||
export MINIO_ROOT_PASSWORD=silo1234
|
||||
export MC_HOST_silo="http://silo:silo1234@127.0.0.1:${start_port}/"
|
||||
unset MINIO_KMS_AUTO_ENCRYPTION # do not auto-encrypt objects
|
||||
export MINIO_CI_CD=1
|
||||
|
||||
@@ -71,7 +71,7 @@ function start_silo_10drive() {
|
||||
"${PWD}/mc" mb --with-versioning silo/bucket
|
||||
|
||||
export AWS_ACCESS_KEY_ID=silo
|
||||
export AWS_SECRET_ACCESS_KEY=silo123
|
||||
export AWS_SECRET_ACCESS_KEY=silo1234
|
||||
aws --endpoint-url http://localhost:"$start_port" s3api create-multipart-upload --bucket bucket --key obj-1 >upload-id.json
|
||||
uploadId=$(jq -r '.UploadId' upload-id.json)
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -30,7 +30,7 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
const manifestVersion = 3
|
||||
const manifestVersion = 4
|
||||
|
||||
var (
|
||||
minioImportRE = regexp.MustCompile(`github\.com/minio/[A-Za-z0-9_./-]+`)
|
||||
@@ -44,19 +44,18 @@ var (
|
||||
)
|
||||
|
||||
type manifest struct {
|
||||
Version int `json:"version"`
|
||||
ModulePath string `json:"module_path"`
|
||||
MinioImports []string `json:"minio_imports"`
|
||||
Environment []string `json:"environment"`
|
||||
Metrics []string `json:"metrics"`
|
||||
Headers []string `json:"headers"`
|
||||
Routes []string `json:"routes"`
|
||||
RouteRoots []string `json:"route_roots"`
|
||||
GridRoutes []string `json:"grid_routes"`
|
||||
StorageMarkers []string `json:"storage_markers"`
|
||||
PolicyValues []string `json:"policy_values"`
|
||||
ExportedSymbols []string `json:"exported_symbols"`
|
||||
BrandAllowlist []string `json:"brand_allowlist"`
|
||||
Version int `json:"version"`
|
||||
ModulePath string `json:"module_path"`
|
||||
MinioImports []string `json:"minio_imports"`
|
||||
Environment []string `json:"environment"`
|
||||
Metrics []string `json:"metrics"`
|
||||
Headers []string `json:"headers"`
|
||||
Routes []string `json:"routes"`
|
||||
RouteRoots []string `json:"route_roots"`
|
||||
GridRoutes []string `json:"grid_routes"`
|
||||
StorageMarkers []string `json:"storage_markers"`
|
||||
PolicyValues []string `json:"policy_values"`
|
||||
BrandAllowlist []string `json:"brand_allowlist"`
|
||||
}
|
||||
|
||||
func main() {
|
||||
@@ -101,17 +100,16 @@ func collect(repo string) (manifest, error) {
|
||||
}
|
||||
|
||||
sets := map[string]map[string]struct{}{
|
||||
"imports": {},
|
||||
"env": {},
|
||||
"metrics": {},
|
||||
"headers": {},
|
||||
"routes": {},
|
||||
"roots": {},
|
||||
"grid": {},
|
||||
"storage": {},
|
||||
"policy": {},
|
||||
"exported": {},
|
||||
"brand": {},
|
||||
"imports": {},
|
||||
"env": {},
|
||||
"metrics": {},
|
||||
"headers": {},
|
||||
"routes": {},
|
||||
"roots": {},
|
||||
"grid": {},
|
||||
"storage": {},
|
||||
"policy": {},
|
||||
"brand": {},
|
||||
}
|
||||
modulePath := ""
|
||||
fset := token.NewFileSet()
|
||||
@@ -163,17 +161,17 @@ func collect(repo string) (manifest, error) {
|
||||
sets["imports"][value] = struct{}{}
|
||||
}
|
||||
}
|
||||
collectStringMatches(sets["routes"], routeRE, file)
|
||||
collectNamedStringValues(sets["roots"], rel, file, "minioReservedBucket")
|
||||
if rel == "internal/grid/manager.go" {
|
||||
collectStringMatches(sets["grid"], routeRE, file)
|
||||
}
|
||||
if !strings.HasSuffix(rel, "_test.go") {
|
||||
collectExported(sets["exported"], filepath.ToSlash(filepath.Dir(rel)), file)
|
||||
// Test files hold request paths for fixtures, not served routes.
|
||||
collectStringMatches(sets["routes"], routeRE, file)
|
||||
if strings.HasPrefix(rel, "cmd/") || strings.HasPrefix(rel, "internal/") {
|
||||
collectBrandStrings(sets["brand"], rel, file)
|
||||
}
|
||||
}
|
||||
collectNamedStringValues(sets["roots"], rel, file, "minioReservedBucket")
|
||||
if rel == "internal/grid/manager.go" {
|
||||
collectStringMatches(sets["grid"], routeRE, file)
|
||||
}
|
||||
}
|
||||
}
|
||||
// This was a shell-local PID variable in the generated inspect script,
|
||||
@@ -184,19 +182,18 @@ func collect(repo string) (manifest, error) {
|
||||
return manifest{}, errors.New("go.mod module path was not found")
|
||||
}
|
||||
return manifest{
|
||||
Version: manifestVersion,
|
||||
ModulePath: modulePath,
|
||||
MinioImports: sorted(sets["imports"]),
|
||||
Environment: sorted(sets["env"]),
|
||||
Metrics: sorted(sets["metrics"]),
|
||||
Headers: sorted(sets["headers"]),
|
||||
Routes: sorted(sets["routes"]),
|
||||
RouteRoots: sorted(sets["roots"]),
|
||||
GridRoutes: sorted(sets["grid"]),
|
||||
StorageMarkers: sorted(sets["storage"]),
|
||||
PolicyValues: sorted(sets["policy"]),
|
||||
ExportedSymbols: sorted(sets["exported"]),
|
||||
BrandAllowlist: sorted(sets["brand"]),
|
||||
Version: manifestVersion,
|
||||
ModulePath: modulePath,
|
||||
MinioImports: sorted(sets["imports"]),
|
||||
Environment: sorted(sets["env"]),
|
||||
Metrics: sorted(sets["metrics"]),
|
||||
Headers: sorted(sets["headers"]),
|
||||
Routes: sorted(sets["routes"]),
|
||||
RouteRoots: sorted(sets["roots"]),
|
||||
GridRoutes: sorted(sets["grid"]),
|
||||
StorageMarkers: sorted(sets["storage"]),
|
||||
PolicyValues: sorted(sets["policy"]),
|
||||
BrandAllowlist: sorted(sets["brand"]),
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -262,7 +259,7 @@ func collectStringMatches(dst map[string]struct{}, re *regexp.Regexp, file *ast.
|
||||
}
|
||||
|
||||
func trackedFiles(repo string) ([]string, error) {
|
||||
cmd := exec.Command("git", "-C", repo, "ls-files", "--cached", "--others", "--exclude-standard", "-z")
|
||||
cmd := exec.Command("git", "-C", repo, "ls-files", "--cached", "-z")
|
||||
out, err := cmd.Output()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("git ls-files: %w", err)
|
||||
@@ -286,78 +283,6 @@ func addMatches(dst map[string]struct{}, re *regexp.Regexp, text string, lower b
|
||||
}
|
||||
}
|
||||
|
||||
func collectExported(dst map[string]struct{}, dir string, file *ast.File) {
|
||||
prefix := dir + ":" + file.Name.Name + ":"
|
||||
for _, decl := range file.Decls {
|
||||
switch decl := decl.(type) {
|
||||
case *ast.FuncDecl:
|
||||
if !ast.IsExported(decl.Name.Name) {
|
||||
continue
|
||||
}
|
||||
if decl.Recv == nil {
|
||||
dst[prefix+"func:"+decl.Name.Name] = struct{}{}
|
||||
continue
|
||||
}
|
||||
receiver := receiverName(decl.Recv.List[0].Type)
|
||||
dst[prefix+"method:"+receiver+"."+decl.Name.Name] = struct{}{}
|
||||
case *ast.GenDecl:
|
||||
for _, spec := range decl.Specs {
|
||||
switch spec := spec.(type) {
|
||||
case *ast.TypeSpec:
|
||||
if !ast.IsExported(spec.Name.Name) {
|
||||
continue
|
||||
}
|
||||
dst[prefix+"type:"+spec.Name.Name] = struct{}{}
|
||||
collectExportedFields(dst, prefix, spec.Name.Name, spec.Type)
|
||||
case *ast.ValueSpec:
|
||||
kind := strings.ToLower(decl.Tok.String())
|
||||
for _, name := range spec.Names {
|
||||
if ast.IsExported(name.Name) {
|
||||
dst[prefix+kind+":"+name.Name] = struct{}{}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func collectExportedFields(dst map[string]struct{}, prefix, typeName string, expr ast.Expr) {
|
||||
var fields *ast.FieldList
|
||||
switch typed := expr.(type) {
|
||||
case *ast.StructType:
|
||||
fields = typed.Fields
|
||||
case *ast.InterfaceType:
|
||||
fields = typed.Methods
|
||||
default:
|
||||
return
|
||||
}
|
||||
for _, field := range fields.List {
|
||||
for _, name := range field.Names {
|
||||
if ast.IsExported(name.Name) {
|
||||
dst[prefix+"field:"+typeName+"."+name.Name] = struct{}{}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func receiverName(expr ast.Expr) string {
|
||||
switch expr := expr.(type) {
|
||||
case *ast.Ident:
|
||||
return expr.Name
|
||||
case *ast.StarExpr:
|
||||
return receiverName(expr.X)
|
||||
case *ast.IndexExpr:
|
||||
return receiverName(expr.X)
|
||||
case *ast.IndexListExpr:
|
||||
return receiverName(expr.X)
|
||||
case *ast.SelectorExpr:
|
||||
return receiverName(expr.X) + "." + expr.Sel.Name
|
||||
default:
|
||||
return fmt.Sprintf("%T", expr)
|
||||
}
|
||||
}
|
||||
|
||||
func sorted(set map[string]struct{}) []string {
|
||||
values := make([]string, 0, len(set))
|
||||
for value := range set {
|
||||
@@ -409,7 +334,6 @@ func compare(want, got manifest) error {
|
||||
{"grid_routes", want.GridRoutes, got.GridRoutes},
|
||||
{"storage_markers", want.StorageMarkers, got.StorageMarkers},
|
||||
{"policy_values", want.PolicyValues, got.PolicyValues},
|
||||
{"exported_symbols", want.ExportedSymbols, got.ExportedSymbols},
|
||||
{"brand_allowlist", want.BrandAllowlist, got.BrandAllowlist},
|
||||
}
|
||||
for _, check := range checks {
|
||||
@@ -454,10 +378,10 @@ func setDiff(want, got []string) (missing, added []string) {
|
||||
}
|
||||
|
||||
func printSummary(value manifest) {
|
||||
fmt.Printf("compatibility manifest: imports=%d env=%d metrics=%d headers=%d routes=%d roots=%d grid=%d storage=%d policy=%d exported=%d brand=%d sha256=%s\n",
|
||||
fmt.Printf("compatibility manifest: imports=%d env=%d metrics=%d headers=%d routes=%d roots=%d grid=%d storage=%d policy=%d brand=%d sha256=%s\n",
|
||||
len(value.MinioImports), len(value.Environment), len(value.Metrics), len(value.Headers),
|
||||
len(value.Routes), len(value.RouteRoots), len(value.GridRoutes), len(value.StorageMarkers), len(value.PolicyValues),
|
||||
len(value.ExportedSymbols), len(value.BrandAllowlist), manifestDigest(value))
|
||||
len(value.BrandAllowlist), manifestDigest(value))
|
||||
}
|
||||
|
||||
func manifestDigest(value manifest) string {
|
||||
|
||||
@@ -18,8 +18,8 @@ function start_silo_5drive() {
|
||||
start_port=$1
|
||||
|
||||
export MINIO_ROOT_USER=silo
|
||||
export MINIO_ROOT_PASSWORD=silo123
|
||||
export MC_HOST_silo="http://silo:silo123@127.0.0.1:${start_port}/"
|
||||
export MINIO_ROOT_PASSWORD=silo1234
|
||||
export MC_HOST_silo="http://silo:silo1234@127.0.0.1:${start_port}/"
|
||||
unset MINIO_KMS_AUTO_ENCRYPTION # do not auto-encrypt objects
|
||||
export MINIO_CI_CD=1
|
||||
|
||||
|
||||
@@ -28,8 +28,8 @@ function verify_rewrite() {
|
||||
start_port=$1
|
||||
|
||||
export MINIO_ACCESS_KEY=silo
|
||||
export MINIO_SECRET_KEY=silo123
|
||||
export MC_HOST_silo="http://silo:silo123@127.0.0.1:${start_port}/"
|
||||
export MINIO_SECRET_KEY=silo1234
|
||||
export MC_HOST_silo="http://silo:silo1234@127.0.0.1:${start_port}/"
|
||||
unset MINIO_KMS_AUTO_ENCRYPTION # do not auto-encrypt objects
|
||||
export MINIO_CI_CD=1
|
||||
|
||||
@@ -87,7 +87,7 @@ function verify_rewrite() {
|
||||
-debug \
|
||||
-versions \
|
||||
-access-key silo \
|
||||
-secret-key silo123 \
|
||||
-secret-key silo1234 \
|
||||
-endpoint "http://127.0.0.1:${start_port}/" 2>&1 | grep INTACT; then
|
||||
echo "server1 log:"
|
||||
cat "${WORK_DIR}/server1.log"
|
||||
@@ -105,14 +105,14 @@ function verify_rewrite() {
|
||||
exit 1
|
||||
fi
|
||||
|
||||
go run ./buildscripts/heal-manual.go "127.0.0.1:${start_port}" "silo" "silo123"
|
||||
go run ./buildscripts/heal-manual.go "127.0.0.1:${start_port}" "silo" "silo1234"
|
||||
sleep 1
|
||||
|
||||
if ! ./s3-check-md5 \
|
||||
-debug \
|
||||
-versions \
|
||||
-access-key silo \
|
||||
-secret-key silo123 \
|
||||
-secret-key silo1234 \
|
||||
-endpoint http://127.0.0.1:${start_port}/ 2>&1 | grep INTACT; then
|
||||
echo "server1 log:"
|
||||
cat "${WORK_DIR}/server1.log"
|
||||
|
||||
@@ -74,8 +74,8 @@ function test_silo_with_timeout() {
|
||||
start_port=$1
|
||||
|
||||
export MINIO_ROOT_USER=silo
|
||||
export MINIO_ROOT_PASSWORD=silo123
|
||||
export MC_HOST_silo="http://silo:silo123@127.0.0.1:${start_port}/"
|
||||
export MINIO_ROOT_PASSWORD=silo1234
|
||||
export MC_HOST_silo="http://silo:silo1234@127.0.0.1:${start_port}/"
|
||||
export MINIO_CI_CD=1
|
||||
|
||||
mkdir ${WORK_DIR}
|
||||
|
||||
@@ -15,10 +15,10 @@ WORK_DIR="$PWD/.verify-$RANDOM"
|
||||
export MINT_MODE=core
|
||||
export MINT_DATA_DIR="$WORK_DIR/data"
|
||||
export SERVER_ENDPOINT="127.0.0.1:9000"
|
||||
export MC_HOST_verify="http://silo:silo123@${SERVER_ENDPOINT}/"
|
||||
export MC_HOST_verify_ipv6="http://silo:silo123@[::1]:9000/"
|
||||
export MC_HOST_verify="http://silo:silo1234@${SERVER_ENDPOINT}/"
|
||||
export MC_HOST_verify_ipv6="http://silo:silo1234@[::1]:9000/"
|
||||
export ACCESS_KEY="silo"
|
||||
export SECRET_KEY="silo123"
|
||||
export SECRET_KEY="silo1234"
|
||||
export ENABLE_HTTPS=0
|
||||
export GO111MODULE=on
|
||||
export GOGC=25
|
||||
@@ -225,7 +225,7 @@ function __init__() {
|
||||
shred -n 1 -s 65M - 1>"$FILE_65_MB" 2>/dev/null
|
||||
|
||||
## version is purposefully set to '3' for minio to migrate configuration file
|
||||
echo '{"version": "3", "credential": {"accessKey": "silo", "secretKey": "silo123"}, "region": "us-east-1"}' >"$SILO_CONFIG_DIR/config.json"
|
||||
echo '{"version": "3", "credential": {"accessKey": "silo", "secretKey": "silo1234"}, "region": "us-east-1"}' >"$SILO_CONFIG_DIR/config.json"
|
||||
|
||||
"$(git rev-parse --show-toplevel)/buildscripts/install-verified-fixture.sh" \
|
||||
https://raw.githubusercontent.com/pgsty/mc/4c4dcc4b55baf238cd0c81030d77945b3828f157/functional-tests.sh \
|
||||
|
||||
@@ -15,7 +15,7 @@ SILO=("$PWD/silo" --config-dir "$SILO_CONFIG_DIR" server)
|
||||
|
||||
function start_silo_3_node() {
|
||||
export MINIO_ROOT_USER=silo
|
||||
export MINIO_ROOT_PASSWORD=silo123
|
||||
export MINIO_ROOT_PASSWORD=silo1234
|
||||
export MINIO_ERASURE_SET_DRIVE_COUNT=6
|
||||
export MINIO_CI_CD=1
|
||||
|
||||
@@ -37,7 +37,7 @@ function start_silo_3_node() {
|
||||
pid3=$!
|
||||
disown $pid3
|
||||
|
||||
export MC_HOST_mysilo="http://silo:silo123@127.0.0.1:$((start_port + 1))"
|
||||
export MC_HOST_mysilo="http://silo:silo1234@127.0.0.1:$((start_port + 1))"
|
||||
|
||||
timeout 15m /tmp/mc ready mysilo || fail
|
||||
|
||||
@@ -116,7 +116,7 @@ function __init__() {
|
||||
mkdir -p "$SILO_CONFIG_DIR"
|
||||
|
||||
## version is purposefully set to '3' for minio to migrate configuration file
|
||||
echo '{"version": "3", "credential": {"accessKey": "silo", "secretKey": "silo123"}, "region": "us-east-1"}' >"$SILO_CONFIG_DIR/config.json"
|
||||
echo '{"version": "3", "credential": {"accessKey": "silo", "secretKey": "silo1234"}, "region": "us-east-1"}' >"$SILO_CONFIG_DIR/config.json"
|
||||
|
||||
if [ ! -f /tmp/mc ]; then
|
||||
"$(git rev-parse --show-toplevel)/buildscripts/install-mcli.sh" /tmp/mc
|
||||
|
||||
@@ -17,7 +17,7 @@ function start_silo() {
|
||||
start_port=$1
|
||||
|
||||
export MINIO_ROOT_USER=silo
|
||||
export MINIO_ROOT_PASSWORD=silo123
|
||||
export MINIO_ROOT_PASSWORD=silo1234
|
||||
unset MINIO_KMS_AUTO_ENCRYPTION # do not auto-encrypt objects
|
||||
unset MINIO_CI_CD
|
||||
unset CI
|
||||
|
||||
@@ -20,7 +20,7 @@ function start_silo_3_node() {
|
||||
done
|
||||
|
||||
export MINIO_ROOT_USER=silo
|
||||
export MINIO_ROOT_PASSWORD=silo123
|
||||
export MINIO_ROOT_PASSWORD=silo1234
|
||||
export MINIO_ERASURE_SET_DRIVE_COUNT=6
|
||||
export MINIO_CI_CD=1
|
||||
|
||||
@@ -46,7 +46,7 @@ function start_silo_3_node() {
|
||||
pid3=$!
|
||||
disown $pid3
|
||||
|
||||
export MC_HOST_mysilo="http://silo:silo123@127.0.0.1:$((start_port + 1))"
|
||||
export MC_HOST_mysilo="http://silo:silo1234@127.0.0.1:$((start_port + 1))"
|
||||
timeout 15m /tmp/mc ready mysilo || fail
|
||||
|
||||
[ ${first_time} -eq 0 ] && upload_objects
|
||||
@@ -117,7 +117,7 @@ function __init__() {
|
||||
mkdir -p "$SILO_CONFIG_DIR"
|
||||
|
||||
## version is purposefully set to '3' for minio to migrate configuration file
|
||||
echo '{"version": "3", "credential": {"accessKey": "silo", "secretKey": "silo123"}, "region": "us-east-1"}' >"$SILO_CONFIG_DIR/config.json"
|
||||
echo '{"version": "3", "credential": {"accessKey": "silo", "secretKey": "silo1234"}, "region": "us-east-1"}' >"$SILO_CONFIG_DIR/config.json"
|
||||
|
||||
if [ ! -f /tmp/mc ]; then
|
||||
"$(git rev-parse --show-toplevel)/buildscripts/install-mcli.sh" /tmp/mc
|
||||
|
||||
+10
-12
@@ -461,15 +461,15 @@ func registerAPIRouter(router *mux.Router) {
|
||||
router.Methods(http.MethodPut).
|
||||
HandlerFunc(s3APIMiddleware(api.PutBucketACLHandler)).
|
||||
Queries("acl", "")
|
||||
// GetBucketCors - this is a dummy call.
|
||||
// GetBucketCors
|
||||
router.Methods(http.MethodGet).
|
||||
HandlerFunc(s3APIMiddleware(api.GetBucketCorsHandler)).
|
||||
Queries("cors", "")
|
||||
// PutBucketCors - this is a dummy call.
|
||||
// PutBucketCors
|
||||
router.Methods(http.MethodPut).
|
||||
HandlerFunc(s3APIMiddleware(api.PutBucketCorsHandler)).
|
||||
Queries("cors", "")
|
||||
// DeleteBucketCors - this is a dummy call.
|
||||
// DeleteBucketCors
|
||||
router.Methods(http.MethodDelete).
|
||||
HandlerFunc(s3APIMiddleware(api.DeleteBucketCorsHandler)).
|
||||
Queries("cors", "")
|
||||
@@ -787,15 +787,13 @@ func corsHandler(handler http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Header.Get("Origin") != "" {
|
||||
if bucket, _ := request2BucketObjectName(r); bucket != "" && globalBucketMetadataSys != nil {
|
||||
// Resident-only lookup: this runs pre-auth for every
|
||||
// Origin-bearing request using a client-supplied path segment as
|
||||
// the bucket name. It must never load or cache metadata for
|
||||
// arbitrary names (see GetResidentCorsConfig). GetResidentCorsConfig
|
||||
// is the single decision point: it returns errInvalidArgument for
|
||||
// the internal .minio.sys namespace (fail closed), a config for a
|
||||
// resident bucket, errBucketMetadataNotInitialized for a real but
|
||||
// unloaded bucket (fail closed), and errConfigNotFound otherwise
|
||||
// (fall back to the global policy below).
|
||||
// Resident-only lookup: this runs before authentication with a
|
||||
// client-supplied path segment as the bucket name, so it must
|
||||
// never load or cache metadata. While startup loading is still
|
||||
// running, for a real bucket whose metadata failed to load, and
|
||||
// for a bucket whose stored CORS document failed to parse, the
|
||||
// request gets no CORS headers; any other non-resident name falls
|
||||
// back to the global policy below.
|
||||
cfg, _, err := globalBucketMetadataSys.GetResidentCorsConfig(bucket)
|
||||
if err == nil && cfg != nil {
|
||||
if applyBucketCors(w, r, cfg) {
|
||||
|
||||
@@ -261,6 +261,11 @@ func TestBucketCorsMetadataErrorFailsClosed(t *testing.T) {
|
||||
oldMetadataSys := globalBucketMetadataSys
|
||||
setObjectLayer(nil)
|
||||
globalBucketMetadataSys = NewBucketMetadataSys()
|
||||
// A resident bucket whose stored CORS document does not parse must not be
|
||||
// answered with the global policy: it has a configuration we cannot honor.
|
||||
meta := newBucketMetadata("cors-metadata-error")
|
||||
meta.corsConfigErr = fmt.Errorf("invalid bucket CORS configuration")
|
||||
globalBucketMetadataSys.Set("cors-metadata-error", meta)
|
||||
defer func() {
|
||||
setObjectLayer(oldObjectAPI)
|
||||
globalBucketMetadataSys = oldMetadataSys
|
||||
@@ -706,35 +711,3 @@ func testBucketCorsLoadFailedBucketFailsClosed(obj ObjectLayer, _ string, _ stri
|
||||
t.Fatalf("load-failed CORS lookup performed %d synchronous bucket metadata reads", got)
|
||||
}
|
||||
}
|
||||
|
||||
// TestBucketCorsInternalBucketFailsClosed guards P2: an Origin-bearing request
|
||||
// whose first path segment is the reserved .minio.sys namespace must preserve
|
||||
// GetConfig's errInvalidArgument semantics and fail closed, not fall back to
|
||||
// the permissive global CORS policy.
|
||||
func TestBucketCorsInternalBucketFailsClosed(t *testing.T) {
|
||||
ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{
|
||||
t: t,
|
||||
objAPITest: testBucketCorsInternalBucketFailsClosed,
|
||||
endpoints: []string{"GetBucketCors"},
|
||||
})
|
||||
}
|
||||
|
||||
func testBucketCorsInternalBucketFailsClosed(_ ObjectLayer, _ string, _ string, _ http.Handler, _ auth.Credentials, t *testing.T) {
|
||||
restoreInit := markBucketMetadataInitialized(t)
|
||||
defer restoreInit()
|
||||
|
||||
wrapped := corsHandler(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
}))
|
||||
rec := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodGet, getGetObjectURL("", minioMetaBucket, "object"), nil)
|
||||
req.Header.Set("Origin", "https://app.example.com")
|
||||
wrapped.ServeHTTP(rec, req)
|
||||
|
||||
if got := rec.Header().Get("Access-Control-Allow-Origin"); got != "" {
|
||||
t.Fatalf("internal bucket fell back to global allow-origin %q", got)
|
||||
}
|
||||
if got := rec.Header().Get("Access-Control-Allow-Credentials"); got != "" {
|
||||
t.Fatalf("internal bucket fell back to global credentials %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,7 +85,7 @@ func testPeerBucketCorsReplicationOrdering(_ ObjectLayer, _ string, bucket strin
|
||||
if !meta.CorsConfigUpdatedAt.Equal(putAt) {
|
||||
t.Fatalf("peer PUT timestamp = %v, want source time %v", meta.CorsConfigUpdatedAt, putAt)
|
||||
}
|
||||
cfg, cfgAt, err := globalBucketMetadataSys.GetCorsConfig(bucket)
|
||||
cfg, cfgAt, err := globalBucketMetadataSys.GetResidentCorsConfig(bucket)
|
||||
if err != nil {
|
||||
t.Fatalf("peer PUT stored raw XML but no parsed config: %v", err)
|
||||
}
|
||||
@@ -165,10 +165,10 @@ func TestSiteReplicationMetaInfoPreservesCorsTombstone(t *testing.T) {
|
||||
|
||||
func testSiteReplicationMetaInfoPreservesCorsTombstone(obj ObjectLayer, _ string, bucket string, _ http.Handler, _ auth.Credentials, t *testing.T) {
|
||||
ctx := t.Context()
|
||||
if _, err := globalBucketMetadataSys.Update(ctx, bucket, bucketCorsConfig, []byte(testSiteReplicationCORSDoc)); err != nil {
|
||||
if _, err := updateLocalBucketCORSMetadata(ctx, obj, bucket, []byte(testSiteReplicationCORSDoc)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
deleteAt, err := globalBucketMetadataSys.Delete(ctx, bucket, bucketCorsConfig)
|
||||
deleteAt, err := updateLocalBucketCORSMetadata(ctx, obj, bucket, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -918,8 +918,8 @@ func testLegacyInvalidCorsMetadataCanBeDeleted(obj ObjectLayer, _ string, bucket
|
||||
t.Fatalf("legacy CORS state = (%#v, %v), want fail-closed parse error", loaded.corsConfig, loaded.corsConfigErr)
|
||||
}
|
||||
globalBucketMetadataSys.Set(bucket, loaded)
|
||||
if _, gotAt, err := globalBucketMetadataSys.GetCorsConfig(bucket); err == nil || !gotAt.Equal(legacyAt) {
|
||||
t.Fatalf("GetCorsConfig = timestamp %v, error %v; want legacy timestamp and error", gotAt, err)
|
||||
if _, gotAt, err := globalBucketMetadataSys.GetResidentCorsConfig(bucket); err == nil || !gotAt.Equal(legacyAt) {
|
||||
t.Fatalf("GetResidentCorsConfig = timestamp %v, error %v; want legacy timestamp and error", gotAt, err)
|
||||
}
|
||||
if _, gotAt, err := globalBucketMetadataSys.GetCorsConfigXML(bucket); err == nil || !gotAt.Equal(legacyAt) {
|
||||
t.Fatalf("GetCorsConfigXML = timestamp %v, error %v; want legacy timestamp and error", gotAt, err)
|
||||
|
||||
@@ -509,10 +509,6 @@ func (api objectAPIHandlers) DeleteMultipleObjectsHandler(w http.ResponseWriter,
|
||||
reqInfo.ObjectName = object.ObjectName
|
||||
reqInfo.VersionID = object.VersionID
|
||||
if apiErrCode := authorizeRequest(ctx, r, deleteObjectAction(object.VersionID)); apiErrCode != ErrNone {
|
||||
if apiErrCode == ErrSignatureDoesNotMatch || apiErrCode == ErrInvalidAccessKeyID {
|
||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(apiErrCode), r.URL)
|
||||
return
|
||||
}
|
||||
apiErr := errorCodes.ToAPIErr(apiErrCode)
|
||||
deleteResults[index].errInfo = DeleteError{
|
||||
Code: apiErr.Code,
|
||||
|
||||
+38
-82
@@ -51,16 +51,19 @@ type BucketMetadataSys struct {
|
||||
initialized bool
|
||||
group *singleflight.Group
|
||||
metadataMap map[string]BucketMetadata
|
||||
// loadFailed tracks real buckets whose metadata could not be loaded at
|
||||
// startup (concurrentLoad) or during a refresh. Such buckets are NOT
|
||||
// resident in metadataMap even though the subsystem is Initialized, so a
|
||||
// plain map miss cannot distinguish "not a bucket" from "known bucket whose
|
||||
// config we could not read". Callers that must fail closed for a real but
|
||||
// unreadable bucket (e.g. per-bucket CORS) consult this set. It is bounded
|
||||
// by the number of load failures and is empty in normal operation.
|
||||
// loadFailed records real buckets whose metadata could not be loaded at
|
||||
// startup or during a refresh. They are absent from metadataMap even though
|
||||
// the subsystem is initialized, and without this bit a resident-only lookup
|
||||
// could not tell them apart from a name that is not a bucket at all. The
|
||||
// set is bounded by the number of failed loads and empty in normal operation.
|
||||
loadFailed map[string]struct{}
|
||||
}
|
||||
|
||||
// noteLoadFailure and clearLoadFailure maintain loadFailed; both expect the
|
||||
// caller to hold sys.Lock.
|
||||
func (sys *BucketMetadataSys) noteLoadFailure(bucket string) { sys.loadFailed[bucket] = struct{}{} }
|
||||
func (sys *BucketMetadataSys) clearLoadFailure(bucket string) { delete(sys.loadFailed, bucket) }
|
||||
|
||||
// Count returns number of bucket metadata map entries.
|
||||
func (sys *BucketMetadataSys) Count() int {
|
||||
sys.RLock()
|
||||
@@ -75,7 +78,7 @@ func (sys *BucketMetadataSys) Remove(buckets ...string) {
|
||||
for _, bucket := range buckets {
|
||||
sys.group.Forget(bucket)
|
||||
delete(sys.metadataMap, bucket)
|
||||
delete(sys.loadFailed, bucket)
|
||||
sys.clearLoadFailure(bucket)
|
||||
globalBucketMonitor.DeleteBucket(bucket)
|
||||
}
|
||||
sys.Unlock()
|
||||
@@ -95,7 +98,7 @@ func (sys *BucketMetadataSys) RemoveStaleBuckets(diskBuckets set.StringSet) {
|
||||
}
|
||||
for bucket := range sys.loadFailed {
|
||||
if !diskBuckets.Contains(bucket) {
|
||||
delete(sys.loadFailed, bucket)
|
||||
sys.clearLoadFailure(bucket)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -109,7 +112,7 @@ func (sys *BucketMetadataSys) Set(bucket string, meta BucketMetadata) {
|
||||
if !isMinioMetaBucketName(bucket) {
|
||||
sys.Lock()
|
||||
sys.metadataMap[bucket] = meta
|
||||
delete(sys.loadFailed, bucket)
|
||||
sys.clearLoadFailure(bucket)
|
||||
sys.Unlock()
|
||||
}
|
||||
}
|
||||
@@ -163,9 +166,6 @@ func (sys *BucketMetadataSys) updateAndParse(ctx context.Context, bucket string,
|
||||
case bucketTaggingConfig:
|
||||
meta.TaggingConfigXML = configData
|
||||
meta.TaggingConfigUpdatedAt = updatedAt
|
||||
case bucketCorsConfig:
|
||||
meta.CorsConfigXML = configData
|
||||
meta.CorsConfigUpdatedAt = updatedAt
|
||||
case bucketQuotaConfigFile:
|
||||
meta.QuotaConfigJSON = configData
|
||||
meta.QuotaConfigUpdatedAt = updatedAt
|
||||
@@ -415,51 +415,17 @@ func (sys *BucketMetadataSys) GetSSEConfig(bucket string) (*bucketsse.BucketSSEC
|
||||
return meta.sseConfig, meta.EncryptionConfigUpdatedAt, nil
|
||||
}
|
||||
|
||||
// GetCorsConfig returns the CORS configuration for the given bucket.
|
||||
// The returned object must not be modified.
|
||||
func (sys *BucketMetadataSys) GetCorsConfig(bucket string) (*cors.Config, time.Time, error) {
|
||||
meta, _, err := sys.GetConfig(GlobalContext, bucket)
|
||||
if err != nil {
|
||||
return nil, time.Time{}, err
|
||||
}
|
||||
if meta.corsConfigErr != nil {
|
||||
return nil, meta.CorsConfigUpdatedAt, meta.corsConfigErr
|
||||
}
|
||||
if meta.corsConfig == nil {
|
||||
return nil, time.Time{}, errConfigNotFound
|
||||
}
|
||||
return meta.corsConfig, meta.CorsConfigUpdatedAt, nil
|
||||
}
|
||||
|
||||
// GetResidentCorsConfig returns the CORS configuration for the given bucket
|
||||
// using only bucket metadata that is already resident in memory. Unlike
|
||||
// GetCorsConfig it never loads metadata from disk and never caches a new
|
||||
// entry.
|
||||
//
|
||||
// The per-request CORS middleware runs before authentication, for every
|
||||
// Origin-bearing request, using the validated first path segment as the bucket
|
||||
// name.
|
||||
// Routing that through GetCorsConfig (which loads and caches) let an
|
||||
// unauthenticated client grow metadataMap without bound and trigger an
|
||||
// erasure metadata probe for every distinct, attacker-controlled,
|
||||
// non-existent name it sent with an Origin header (e.g. /minio/... , /api/... ,
|
||||
// or random buckets). Every bucket that can carry a CORS document is made
|
||||
// resident when the document is written (Set) and when metadata is loaded at
|
||||
// startup (Init/concurrentLoad), so a resident-only read is complete for real
|
||||
// buckets while costing only an in-memory map lookup for everything else.
|
||||
//
|
||||
// While bucket metadata is still loading (not yet Initialized) a non-resident
|
||||
// bucket returns errBucketMetadataNotInitialized so the caller fails closed
|
||||
// rather than answering with the permissive global policy for a bucket whose
|
||||
// restrictive CORS document may simply not be loaded yet.
|
||||
// GetResidentCorsConfig returns the CORS configuration of a bucket whose
|
||||
// metadata is already resident in memory. It runs before authentication for
|
||||
// every Origin-bearing request with a client-supplied path segment, so it
|
||||
// never loads or caches metadata. A non-resident name gets no CORS answer
|
||||
// (errBucketMetadataNotInitialized) while startup loading is still running,
|
||||
// and afterwards when it is a real bucket whose metadata failed to load: a
|
||||
// presigned URL is authenticated on its own, so the bucket's CORS document is
|
||||
// the only origin boundary a browser enforces for it. Any other non-resident
|
||||
// name reports errConfigNotFound and the caller applies the global CORS
|
||||
// policy exactly as releases without per-bucket CORS did.
|
||||
func (sys *BucketMetadataSys) GetResidentCorsConfig(bucket string) (*cors.Config, time.Time, error) {
|
||||
if isMinioMetaBucketName(bucket) {
|
||||
// Preserve GetConfig's semantics for the internal namespace: this is
|
||||
// not a real bucket, and returning a non-errConfigNotFound error makes
|
||||
// the CORS middleware fail closed rather than answer for .minio.sys
|
||||
// with the permissive global policy.
|
||||
return nil, time.Time{}, errInvalidArgument
|
||||
}
|
||||
if isReservedOrInvalidBucket(bucket, true) {
|
||||
return nil, time.Time{}, errConfigNotFound
|
||||
}
|
||||
@@ -468,26 +434,19 @@ func (sys *BucketMetadataSys) GetResidentCorsConfig(bucket string) (*cors.Config
|
||||
_, failed := sys.loadFailed[bucket]
|
||||
initialized := sys.initialized
|
||||
sys.RUnlock()
|
||||
if ok {
|
||||
if meta.corsConfigErr != nil {
|
||||
return nil, meta.CorsConfigUpdatedAt, meta.corsConfigErr
|
||||
if !ok {
|
||||
if !initialized || failed {
|
||||
return nil, time.Time{}, errBucketMetadataNotInitialized
|
||||
}
|
||||
if meta.corsConfig == nil {
|
||||
return nil, time.Time{}, errConfigNotFound
|
||||
}
|
||||
return meta.corsConfig, meta.CorsConfigUpdatedAt, nil
|
||||
return nil, time.Time{}, errConfigNotFound
|
||||
}
|
||||
// Not resident. Two cases must not be conflated:
|
||||
// - metadata is still loading (!initialized), or this is a real bucket
|
||||
// whose metadata failed to load: we cannot rule out a restrictive CORS
|
||||
// config, so fail closed rather than answer with the global policy.
|
||||
// - a fully initialized subsystem with no record of the name: it is not a
|
||||
// bucket that can carry CORS, so fall back to the global policy without
|
||||
// loading or caching metadata for an arbitrary, client-supplied name.
|
||||
if !initialized || failed {
|
||||
return nil, time.Time{}, errBucketMetadataNotInitialized
|
||||
if meta.corsConfigErr != nil {
|
||||
return nil, meta.CorsConfigUpdatedAt, meta.corsConfigErr
|
||||
}
|
||||
return nil, time.Time{}, errConfigNotFound
|
||||
if meta.corsConfig == nil {
|
||||
return nil, time.Time{}, errConfigNotFound
|
||||
}
|
||||
return meta.corsConfig, meta.CorsConfigUpdatedAt, nil
|
||||
}
|
||||
|
||||
// GetCorsConfigXML returns the raw stored CORS configuration XML for the
|
||||
@@ -636,6 +595,7 @@ func (sys *BucketMetadataSys) GetConfig(ctx context.Context, bucket string) (met
|
||||
}
|
||||
sys.Lock()
|
||||
sys.metadataMap[bucket] = meta
|
||||
sys.clearLoadFailure(bucket)
|
||||
sys.Unlock()
|
||||
|
||||
return meta, true, nil
|
||||
@@ -687,13 +647,10 @@ func (sys *BucketMetadataSys) concurrentLoad(ctx context.Context, buckets []stri
|
||||
sys.Lock()
|
||||
for i, meta := range bucketMetas {
|
||||
if errs[i] != nil {
|
||||
// Real bucket whose metadata could not be loaded: record it so
|
||||
// consumers that must fail closed (per-bucket CORS) can tell it
|
||||
// apart from a name that is not a bucket at all.
|
||||
sys.loadFailed[buckets[i]] = struct{}{}
|
||||
sys.noteLoadFailure(buckets[i])
|
||||
continue
|
||||
}
|
||||
delete(sys.loadFailed, buckets[i])
|
||||
sys.clearLoadFailure(buckets[i])
|
||||
sys.metadataMap[buckets[i]] = meta
|
||||
}
|
||||
sys.Unlock()
|
||||
@@ -744,7 +701,7 @@ func (sys *BucketMetadataSys) refreshBucketsMetadataLoop(ctx context.Context) {
|
||||
if err != nil {
|
||||
internalLogIf(ctx, err, logger.WarningKind)
|
||||
sys.Lock()
|
||||
sys.loadFailed[bucket] = struct{}{}
|
||||
sys.noteLoadFailure(bucket)
|
||||
sys.Unlock()
|
||||
wait() // wait to proceed to next entry.
|
||||
continue
|
||||
@@ -756,8 +713,7 @@ func (sys *BucketMetadataSys) refreshBucketsMetadataLoop(ctx context.Context) {
|
||||
updated = true
|
||||
sys.metadataMap[bucket] = meta
|
||||
}
|
||||
// A successful (re)load clears any earlier load failure.
|
||||
delete(sys.loadFailed, bucket)
|
||||
sys.clearLoadFailure(bucket)
|
||||
sys.Unlock()
|
||||
|
||||
if updated {
|
||||
|
||||
@@ -377,13 +377,6 @@ func (b *BucketMetadata) parseAllConfigs(ctx context.Context, objectAPI ObjectLa
|
||||
b.corsConfig = nil
|
||||
}
|
||||
|
||||
if bytes.Equal(b.ObjectLockConfigXML, enabledBucketObjectLockConfig) {
|
||||
config, versioningErr := versioning.ParseConfig(bytes.NewReader(b.VersioningConfigXML))
|
||||
if versioningErr != nil || !config.Enabled() {
|
||||
b.VersioningConfigXML = enabledBucketVersioningConfig
|
||||
}
|
||||
}
|
||||
|
||||
if len(b.ObjectLockConfigXML) != 0 {
|
||||
b.objectLockConfig, err = objectlock.ParseObjectLockConfig(bytes.NewReader(b.ObjectLockConfigXML))
|
||||
if err != nil {
|
||||
@@ -392,6 +385,15 @@ func (b *BucketMetadata) parseAllConfigs(ctx context.Context, objectAPI ObjectLa
|
||||
} else {
|
||||
b.objectLockConfig = nil
|
||||
}
|
||||
if b.objectLockConfig != nil {
|
||||
// Object Lock requires every object to be versioned. Whatever the lock
|
||||
// document contains, a suspended or prefix-excluded versioning document
|
||||
// is replaced by plain Enabled versioning; Save persists the result.
|
||||
config, versioningErr := versioning.ParseConfig(bytes.NewReader(b.VersioningConfigXML))
|
||||
if versioningErr != nil || !config.Enabled() || config.PrefixesExcluded() {
|
||||
b.VersioningConfigXML = enabledBucketVersioningConfig
|
||||
}
|
||||
}
|
||||
|
||||
if len(b.VersioningConfigXML) != 0 {
|
||||
b.versioningConfig, err = versioning.ParseConfig(bytes.NewReader(b.VersioningConfigXML))
|
||||
|
||||
@@ -180,12 +180,14 @@ func testDynamicTimeoutAdjust(t *testing.T, timeout *dynamicTimeout, f func() fl
|
||||
func TestDynamicTimeoutAdjustExponential(t *testing.T) {
|
||||
timeout := newDynamicTimeout(time.Minute, time.Second)
|
||||
|
||||
rand.Seed(0)
|
||||
// A private source keeps the sample independent of other tests that use
|
||||
// the global generator concurrently.
|
||||
rng := rand.New(rand.NewSource(0))
|
||||
|
||||
initial := timeout.Timeout()
|
||||
|
||||
for range 10 {
|
||||
testDynamicTimeoutAdjust(t, timeout, rand.ExpFloat64)
|
||||
testDynamicTimeoutAdjust(t, timeout, rng.ExpFloat64)
|
||||
}
|
||||
|
||||
adjusted := timeout.Timeout()
|
||||
@@ -197,13 +199,13 @@ func TestDynamicTimeoutAdjustExponential(t *testing.T) {
|
||||
func TestDynamicTimeoutAdjustNormalized(t *testing.T) {
|
||||
timeout := newDynamicTimeout(time.Minute, time.Second)
|
||||
|
||||
rand.Seed(0)
|
||||
rng := rand.New(rand.NewSource(0))
|
||||
|
||||
initial := timeout.Timeout()
|
||||
|
||||
for range 10 {
|
||||
testDynamicTimeoutAdjust(t, timeout, func() float64 {
|
||||
return 1.0 + rand.NormFloat64()
|
||||
return 1.0 + rng.NormFloat64()
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -355,29 +355,6 @@ func rotateKey(ctx context.Context, oldKey []byte, newKeyID string, newKey []byt
|
||||
}
|
||||
}
|
||||
|
||||
// checkSSECCopySourceKey authenticates the SSE-C copy source key against the
|
||||
// sealed object key held in metadata. This keeps the diverted rotation safe on
|
||||
// its own and remains defense in depth when the read path also authenticates
|
||||
// zero-byte objects. Mirrors the errors rotateKey reports.
|
||||
func checkSSECCopySourceKey(h http.Header, metadata map[string]string, bucket, object string, newKey []byte) error {
|
||||
oldKey, err := ParseSSECopyCustomerRequest(h, metadata)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sealedKey, err := crypto.SSEC.ParseMetadata(metadata)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var objectKey crypto.ObjectKey
|
||||
if err := objectKey.Unseal(oldKey, sealedKey, crypto.SSEC.String(), bucket, object); err != nil {
|
||||
if subtle.ConstantTimeCompare(oldKey, newKey) == 1 {
|
||||
return errInvalidSSEParameters
|
||||
}
|
||||
return crypto.ErrInvalidCustomerKey
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func newEncryptMetadata(ctx context.Context, kind crypto.Type, keyID string, key []byte, bucket, object string, metadata map[string]string, cryptoCtx kms.Context) (crypto.ObjectKey, error) {
|
||||
var sealedKey crypto.SealedKey
|
||||
switch kind {
|
||||
|
||||
@@ -911,7 +911,7 @@ func (z *erasureServerPools) MakeBucket(ctx context.Context, bucket string, opts
|
||||
meta.SetCreatedAt(opts.CreatedAt)
|
||||
}
|
||||
if opts.LockEnabled {
|
||||
if err := enablePeerBucketVersioning(&meta); err != nil {
|
||||
if err := enablePeerBucketVersioning(&meta, true); err != nil {
|
||||
return err
|
||||
}
|
||||
if len(meta.ObjectLockConfigXML) == 0 {
|
||||
@@ -920,7 +920,7 @@ func (z *erasureServerPools) MakeBucket(ctx context.Context, bucket string, opts
|
||||
}
|
||||
}
|
||||
if opts.VersioningEnabled {
|
||||
if err := enablePeerBucketVersioning(&meta); err != nil {
|
||||
if err := enablePeerBucketVersioning(&meta, opts.LockEnabled); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
+9
-41
@@ -1308,18 +1308,11 @@ func (api objectAPIHandlers) CopyObjectHandler(w http.ResponseWriter, r *http.Re
|
||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrInvalidStorageClass), r.URL)
|
||||
return
|
||||
}
|
||||
rawReplica := hasReplicaStatus(r.Header)
|
||||
markerExact := hasReplicationMarker(r.Header)
|
||||
replicationPermitted := false
|
||||
if rawReplica || markerExact {
|
||||
replicationPermitted = replicationPermissionAllowed(ctx, r, dstBucket, dstObject, policy.ReplicateObjectAction)
|
||||
}
|
||||
if rawReplica && !replicationPermitted {
|
||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrAccessDenied), r.URL)
|
||||
trustedReplication, replicaTrusted, trustErr := evaluateReplicationTrust(ctx, r, dstBucket, dstObject, policy.ReplicateObjectAction)
|
||||
if trustErr != ErrNone {
|
||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(trustErr), r.URL)
|
||||
return
|
||||
}
|
||||
trustedReplication := markerExact && replicationPermitted
|
||||
replicaTrusted := trustedReplication && rawReplica
|
||||
if hasReplicationRequestHeaders(r.Header) {
|
||||
ctx, r = applyReplicationTrust(ctx, r, trustedReplication, replicaTrusted)
|
||||
}
|
||||
@@ -1522,17 +1515,6 @@ func (api objectAPIHandlers) CopyObjectHandler(w http.ResponseWriter, r *http.Re
|
||||
canRotateKeyInPlace := !srcInfo.Legacy &&
|
||||
!copyRewritesObjectData(srcInfo.metadataOnly, copySrcOpts, dstOpts)
|
||||
|
||||
// The rotation shortcut authenticates the source key by unsealing it. The
|
||||
// re-encrypting fallback authenticates it only through the source decryptor,
|
||||
// which GetObjectNInfo skips for a zero byte object, so check it here before
|
||||
// the destination is written under the new key.
|
||||
if cpSrcDstSame && sseCopyC && sseC && !chStorageClass && !canRotateKeyInPlace {
|
||||
if err := checkSSECCopySourceKey(r.Header, srcInfo.UserDefined, srcBucket, srcObject, newKey); err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// If src == dst and either
|
||||
// - the object is encrypted using SSE-C and two different SSE-C keys are present
|
||||
// - the object is encrypted using SSE-S3 and the SSE-S3 header is present
|
||||
@@ -2073,14 +2055,9 @@ func (api objectAPIHandlers) PutObjectHandler(w http.ResponseWriter, r *http.Req
|
||||
}
|
||||
}
|
||||
|
||||
rawReplica := hasReplicaStatus(r.Header)
|
||||
markerExact := hasReplicationMarker(r.Header)
|
||||
replicationPermitted := false
|
||||
if rawReplica || markerExact {
|
||||
replicationPermitted = replicationPermissionAllowed(ctx, r, bucket, object, policy.ReplicateObjectAction)
|
||||
}
|
||||
if rawReplica && !replicationPermitted {
|
||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrAccessDenied), r.URL)
|
||||
trustedReplication, replicaTrusted, trustErr := evaluateReplicationTrust(ctx, r, bucket, object, policy.ReplicateObjectAction)
|
||||
if trustErr != ErrNone {
|
||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(trustErr), r.URL)
|
||||
return
|
||||
}
|
||||
if _, ok := r.Header[xhttp.MinIOSourceReplicationCheck]; ok {
|
||||
@@ -2093,8 +2070,6 @@ func (api objectAPIHandlers) PutObjectHandler(w http.ResponseWriter, r *http.Req
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
trustedReplication := markerExact && replicationPermitted
|
||||
replicaTrusted := trustedReplication && rawReplica
|
||||
if hasReplicationRequestHeaders(r.Header) {
|
||||
ctx, r = applyReplicationTrust(ctx, r, trustedReplication, replicaTrusted)
|
||||
}
|
||||
@@ -2832,17 +2807,11 @@ func (api objectAPIHandlers) DeleteObjectHandler(w http.ResponseWriter, r *http.
|
||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL)
|
||||
return
|
||||
}
|
||||
rawReplica := hasReplicaStatus(r.Header)
|
||||
markerExact := hasReplicationMarker(r.Header)
|
||||
replicationPermitted := false
|
||||
if rawReplica || markerExact {
|
||||
replicationPermitted = replicationPermissionAllowed(ctx, r, bucket, object, policy.ReplicateDeleteAction)
|
||||
}
|
||||
if rawReplica && !replicationPermitted {
|
||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrAccessDenied), r.URL)
|
||||
trustedReplication, replica, trustErr := evaluateReplicationTrust(ctx, r, bucket, object, policy.ReplicateDeleteAction)
|
||||
if trustErr != ErrNone {
|
||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(trustErr), r.URL)
|
||||
return
|
||||
}
|
||||
trustedReplication := markerExact && replicationPermitted
|
||||
var s3Error APIErrorCode
|
||||
if trustedReplication {
|
||||
s3Error = authorizeReplicationDelete(ctx, r)
|
||||
@@ -2858,7 +2827,6 @@ func (api objectAPIHandlers) DeleteObjectHandler(w http.ResponseWriter, r *http.
|
||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrReplicationPermissionCheckError), r.URL)
|
||||
return
|
||||
}
|
||||
replica := trustedReplication && rawReplica
|
||||
if hasReplicationRequestHeaders(r.Header) {
|
||||
ctx, r = applyReplicationTrust(ctx, r, trustedReplication, replica)
|
||||
}
|
||||
|
||||
@@ -171,18 +171,11 @@ func (api objectAPIHandlers) NewMultipartUploadHandler(w http.ResponseWriter, r
|
||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL)
|
||||
return
|
||||
}
|
||||
rawReplica := hasReplicaStatus(r.Header)
|
||||
markerExact := hasReplicationMarker(r.Header)
|
||||
replicationPermitted := false
|
||||
if rawReplica || markerExact {
|
||||
replicationPermitted = replicationPermissionAllowed(ctx, r, bucket, object, policy.ReplicateObjectAction)
|
||||
}
|
||||
if rawReplica && !replicationPermitted {
|
||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrAccessDenied), r.URL)
|
||||
trustedReplication, replicaTrusted, trustErr := evaluateReplicationTrust(ctx, r, bucket, object, policy.ReplicateObjectAction)
|
||||
if trustErr != ErrNone {
|
||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(trustErr), r.URL)
|
||||
return
|
||||
}
|
||||
trustedReplication := markerExact && replicationPermitted
|
||||
replicaTrusted := trustedReplication && rawReplica
|
||||
if hasReplicationRequestHeaders(r.Header) {
|
||||
ctx, r = applyReplicationTrust(ctx, r, trustedReplication, replicaTrusted)
|
||||
}
|
||||
@@ -870,17 +863,11 @@ func (api objectAPIHandlers) PutObjectPartHandler(w http.ResponseWriter, r *http
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
rawReplica := hasReplicaStatus(r.Header)
|
||||
markerExact := hasReplicationMarker(r.Header)
|
||||
replicationPermitted := false
|
||||
if rawReplica || markerExact {
|
||||
replicationPermitted = replicationPermissionAllowed(ctx, r, bucket, object, policy.ReplicateObjectAction)
|
||||
}
|
||||
if rawReplica && !replicationPermitted {
|
||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrAccessDenied), r.URL)
|
||||
trustedReplication, _, trustErr := evaluateReplicationTrust(ctx, r, bucket, object, policy.ReplicateObjectAction)
|
||||
if trustErr != ErrNone {
|
||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(trustErr), r.URL)
|
||||
return
|
||||
}
|
||||
trustedReplication := markerExact && replicationPermitted
|
||||
storedReplica := mi.UserDefined[xhttp.AmzBucketReplicationStatus] == replication.Replica.String()
|
||||
replicaTrusted := trustedReplication && storedReplica
|
||||
if hasReplicationRequestHeaders(r.Header) {
|
||||
@@ -1110,19 +1097,13 @@ func (api objectAPIHandlers) CompleteMultipartUploadHandler(w http.ResponseWrite
|
||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL)
|
||||
return
|
||||
}
|
||||
rawReplica := hasReplicaStatus(r.Header)
|
||||
markerExact := hasReplicationMarker(r.Header)
|
||||
replicationPermitted := false
|
||||
if rawReplica || markerExact {
|
||||
replicationPermitted = replicationPermissionAllowed(ctx, r, bucket, object, policy.ReplicateObjectAction)
|
||||
}
|
||||
if rawReplica && !replicationPermitted {
|
||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrAccessDenied), r.URL)
|
||||
trustedReplication, replicaTrusted, trustErr := evaluateReplicationTrust(ctx, r, bucket, object, policy.ReplicateObjectAction)
|
||||
if trustErr != ErrNone {
|
||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(trustErr), r.URL)
|
||||
return
|
||||
}
|
||||
trustedReplication := markerExact && replicationPermitted
|
||||
if hasReplicationRequestHeaders(r.Header) {
|
||||
ctx, r = applyReplicationTrust(ctx, r, trustedReplication, trustedReplication && rawReplica)
|
||||
ctx, r = applyReplicationTrust(ctx, r, trustedReplication, replicaTrusted)
|
||||
}
|
||||
|
||||
// Get upload id.
|
||||
|
||||
@@ -70,6 +70,25 @@ func replicationPermissionAllowed(ctx context.Context, r *http.Request, bucket,
|
||||
return authorizeRequest(ctx, r, action) == ErrNone
|
||||
}
|
||||
|
||||
// evaluateReplicationTrust decides whether a request may carry replication
|
||||
// semantics for the given action. A request that declares itself a replica
|
||||
// without holding the replication permission is rejected. trusted reports that
|
||||
// the exact marker came from a permitted principal; replica additionally
|
||||
// requires the request to declare REPLICA status.
|
||||
func evaluateReplicationTrust(ctx context.Context, r *http.Request, bucket, object string, action policy.Action) (trusted, replica bool, s3Err APIErrorCode) {
|
||||
rawReplica := hasReplicaStatus(r.Header)
|
||||
markerExact := hasReplicationMarker(r.Header)
|
||||
permitted := false
|
||||
if rawReplica || markerExact {
|
||||
permitted = replicationPermissionAllowed(ctx, r, bucket, object, action)
|
||||
}
|
||||
if rawReplica && !permitted {
|
||||
return false, false, ErrAccessDenied
|
||||
}
|
||||
trusted = markerExact && permitted
|
||||
return trusted, trusted && rawReplica, ErrNone
|
||||
}
|
||||
|
||||
// replicationRequestHeaders are internal request controls. They are removed
|
||||
// only after signature verification when a request has not earned replication
|
||||
// trust. Public S3/SSE/checksum headers, proxy loop guards, and replication
|
||||
@@ -110,6 +129,9 @@ func hasReplicationRequestHeaders(h http.Header) bool {
|
||||
func cloneRequestWithoutReplicationHeaders(ctx context.Context, r *http.Request) *http.Request {
|
||||
clone := r.Clone(ctx)
|
||||
stripReplicationRequestHeaders(clone.Header)
|
||||
// A streaming body reader built from the original request fills r.Trailer
|
||||
// as the body is consumed; the checksum reader must observe that same map.
|
||||
clone.Trailer = r.Trailer
|
||||
return clone
|
||||
}
|
||||
|
||||
|
||||
@@ -830,3 +830,57 @@ func testAPISSECMultipartReplicationTrust(obj ObjectLayer, instanceType, bucketN
|
||||
t.Fatal("replicated SSE-C multipart object did not decrypt to source plaintext")
|
||||
}
|
||||
}
|
||||
|
||||
// TestAPIStreamingTrailerWithUntrustedReplicationHeaders verifies that a
|
||||
// request which does not earn replication trust is still processed as an
|
||||
// ordinary upload. The streaming body reader fills the original request's
|
||||
// trailer while the handler continues with a header-stripped clone, so the
|
||||
// trailing checksum must remain visible through that clone.
|
||||
func TestAPIStreamingTrailerWithUntrustedReplicationHeaders(t *testing.T) {
|
||||
ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{t: t, objAPITest: testAPIStreamingTrailerWithUntrustedReplicationHeaders})
|
||||
}
|
||||
|
||||
func testAPIStreamingTrailerWithUntrustedReplicationHeaders(obj ObjectLayer, instanceType, bucketName string, apiRouter http.Handler, _ auth.Credentials, t *testing.T) {
|
||||
putOnly := newObjectAttributesAuthzUser(t, instanceType, bucketName, `"s3:PutObject"`)
|
||||
payload := bytes.Repeat([]byte("trailer probe "), 4096)
|
||||
send := func(targetURL string) *httptest.ResponseRecorder {
|
||||
req, err := newStreamingUnsignedTrailerRequest(http.MethodPut, targetURL, payload, UTCNow())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
req.Header.Set(xhttp.MinIOSourceReplicationRequest, "true")
|
||||
req.Header.Set(xhttp.MinIOSourceETag, "forged-etag")
|
||||
if err := signRequestV4(req, putOnly.AccessKey, putOnly.SecretKey); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
rec := httptest.NewRecorder()
|
||||
apiRouter.ServeHTTP(rec, req)
|
||||
return rec
|
||||
}
|
||||
|
||||
if rec := send(getPutObjectURL("", bucketName, "trailer-object")); rec.Code != http.StatusOK {
|
||||
t.Fatalf("%s: PutObject status %d: %s", instanceType, rec.Code, rec.Body.String())
|
||||
}
|
||||
info, err := obj.GetObjectInfo(t.Context(), bucketName, "trailer-object", ObjectOptions{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if info.Size != int64(len(payload)) || info.ETag == "forged-etag" {
|
||||
t.Fatalf("%s: stored size %d etag %q, want %d bytes with a computed etag", instanceType, info.Size, info.ETag, len(payload))
|
||||
}
|
||||
|
||||
upload, err := obj.NewMultipartUpload(t.Context(), bucketName, "trailer-multipart", ObjectOptions{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if rec := send(getPutObjectPartURL("", bucketName, "trailer-multipart", upload.UploadID, "1")); rec.Code != http.StatusOK {
|
||||
t.Fatalf("%s: PutObjectPart status %d: %s", instanceType, rec.Code, rec.Body.String())
|
||||
}
|
||||
parts, err := obj.ListObjectParts(t.Context(), bucketName, "trailer-multipart", upload.UploadID, 0, 10, ObjectOptions{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(parts.Parts) != 1 || parts.Parts[0].Size != int64(len(payload)) {
|
||||
t.Fatalf("%s: uploaded parts %+v, want one part of %d bytes", instanceType, parts.Parts, len(payload))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,7 +39,9 @@ func testPeerBucketAdoptionPreservesLockAndVersioningConfigs(_ ObjectLayer, inst
|
||||
_ http.Handler, _ auth.Credentials, t *testing.T,
|
||||
) {
|
||||
objectLockXML := []byte(`<ObjectLockConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/"><ObjectLockEnabled>Enabled</ObjectLockEnabled><Rule><DefaultRetention><Mode>GOVERNANCE</Mode><Days>30</Days></DefaultRetention></Rule></ObjectLockConfiguration>`)
|
||||
versioningXML := []byte(`<VersioningConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/"><Status>Enabled</Status><ExcludeFolders>true</ExcludeFolders><ExcludedPrefixes><Prefix>temporary/</Prefix></ExcludedPrefixes></VersioningConfiguration>`)
|
||||
// A locked bucket carries plain Enabled versioning; adoption must keep the
|
||||
// existing document and its timestamp rather than rewrite them.
|
||||
versioningXML := []byte(`<VersioningConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/"><Status>Enabled</Status></VersioningConfiguration>`)
|
||||
if _, err := globalBucketMetadataSys.Update(t.Context(), bucketName, objectLockConfig, objectLockXML); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -77,15 +79,15 @@ func TestPeerBucketAdoptionBootstrapsMissingConfigs(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestPeerBucketAdoptionPreservesCustomVersioningWhenEnablingLock(t *testing.T) {
|
||||
func TestPeerBucketAdoptionNormalizesVersioningWhenEnablingLock(t *testing.T) {
|
||||
defer DetectTestLeak(t)()
|
||||
ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{
|
||||
t: t,
|
||||
objAPITest: testPeerBucketAdoptionPreservesCustomVersioningWhenEnablingLock,
|
||||
objAPITest: testPeerBucketAdoptionNormalizesVersioningWhenEnablingLock,
|
||||
})
|
||||
}
|
||||
|
||||
func testPeerBucketAdoptionPreservesCustomVersioningWhenEnablingLock(_ ObjectLayer, instanceType, bucketName string,
|
||||
func testPeerBucketAdoptionNormalizesVersioningWhenEnablingLock(_ ObjectLayer, instanceType, bucketName string,
|
||||
_ http.Handler, _ auth.Credentials, t *testing.T,
|
||||
) {
|
||||
versioningXML := []byte(`<VersioningConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/"><Status>Enabled</Status><ExcludeFolders>true</ExcludeFolders><ExcludedPrefixes><Prefix>temporary/</Prefix></ExcludedPrefixes></VersioningConfiguration>`)
|
||||
@@ -106,8 +108,8 @@ func testPeerBucketAdoptionPreservesCustomVersioningWhenEnablingLock(_ ObjectLay
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !bytes.Equal(after.VersioningConfigXML, before.VersioningConfigXML) || !after.VersioningConfigUpdatedAt.Equal(before.VersioningConfigUpdatedAt) {
|
||||
t.Fatalf("%s: custom versioning changed while enabling Object Lock", instanceType)
|
||||
if !bytes.Equal(after.VersioningConfigXML, enabledBucketVersioningConfig) || !after.VersioningConfigUpdatedAt.After(before.VersioningConfigUpdatedAt) {
|
||||
t.Fatalf("%s: prefix-excluded versioning survived enabling Object Lock: %q", instanceType, after.VersioningConfigXML)
|
||||
}
|
||||
if !bytes.Equal(after.ObjectLockConfigXML, enabledBucketObjectLockConfig) {
|
||||
t.Fatalf("%s: Object Lock was not bootstrapped", instanceType)
|
||||
@@ -152,7 +154,7 @@ func TestEnablePeerBucketVersioningRepairsInvalidConfig(t *testing.T) {
|
||||
meta := newBucketMetadata("bucket")
|
||||
meta.Created = time.Date(2026, time.August, 29, 8, 0, 0, 0, time.UTC)
|
||||
meta.VersioningConfigXML = []byte(`<VersioningConfiguration>`)
|
||||
if err := enablePeerBucketVersioning(&meta); err != nil {
|
||||
if err := enablePeerBucketVersioning(&meta, false); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !bytes.Equal(meta.VersioningConfigXML, enabledBucketVersioningConfig) || meta.VersioningConfigUpdatedAt.IsZero() {
|
||||
@@ -188,3 +190,48 @@ func testPeerBucketAdoptionBootstrapsMissingConfigs(_ ObjectLayer, instanceType,
|
||||
after.ObjectLockConfigUpdatedAt, after.VersioningConfigUpdatedAt, before.Created)
|
||||
}
|
||||
}
|
||||
|
||||
// TestLockedBucketNormalizesVersioningOnSave covers the metadata boundary
|
||||
// itself: whatever writer stores a suspended or prefix-excluded versioning
|
||||
// document on a bucket that carries an Object Lock configuration, including
|
||||
// one with a default retention rule, Save replaces it with plain Enabled
|
||||
// versioning.
|
||||
func TestLockedBucketNormalizesVersioningOnSave(t *testing.T) {
|
||||
defer DetectTestLeak(t)()
|
||||
ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{
|
||||
t: t,
|
||||
objAPITest: testLockedBucketNormalizesVersioningOnSave,
|
||||
makeBucketOptions: MakeBucketOptions{LockEnabled: true},
|
||||
})
|
||||
}
|
||||
|
||||
func testLockedBucketNormalizesVersioningOnSave(_ ObjectLayer, instanceType, bucketName string,
|
||||
_ http.Handler, _ auth.Credentials, t *testing.T,
|
||||
) {
|
||||
lockWithRule := []byte(`<ObjectLockConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/"><ObjectLockEnabled>Enabled</ObjectLockEnabled><Rule><DefaultRetention><Mode>GOVERNANCE</Mode><Days>30</Days></DefaultRetention></Rule></ObjectLockConfiguration>`)
|
||||
if _, err := globalBucketMetadataSys.Update(t.Context(), bucketName, objectLockConfig, lockWithRule); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for name, versioningXML := range map[string][]byte{
|
||||
"prefix-excluded": []byte(`<VersioningConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/"><Status>Enabled</Status><ExcludeFolders>true</ExcludeFolders><ExcludedPrefixes><Prefix>temporary/</Prefix></ExcludedPrefixes></VersioningConfiguration>`),
|
||||
"suspended": []byte(`<VersioningConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/"><Status>Suspended</Status></VersioningConfiguration>`),
|
||||
} {
|
||||
if _, err := globalBucketMetadataSys.Update(t.Context(), bucketName, bucketVersioningConfig, versioningXML); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
meta, err := globalBucketMetadataSys.Get(bucketName)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !bytes.Equal(meta.VersioningConfigXML, enabledBucketVersioningConfig) {
|
||||
t.Fatalf("%s/%s: locked bucket kept versioning %q", instanceType, name, meta.VersioningConfigXML)
|
||||
}
|
||||
reloaded, err := loadBucketMetadata(t.Context(), newObjectLayerFn(), bucketName)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !bytes.Equal(reloaded.VersioningConfigXML, enabledBucketVersioningConfig) {
|
||||
t.Fatalf("%s/%s: locked bucket persisted versioning %q", instanceType, name, reloaded.VersioningConfigXML)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -159,8 +159,10 @@ func testPeerBucketObjectLockMetadataWithoutLockEnabled(_ ObjectLayer, instanceT
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if meta.objectLockConfig == nil || len(meta.VersioningConfigXML) != 0 {
|
||||
t.Fatalf("%s: unlocked bucket metadata = objectLock:%v versioning:%q", instanceType, meta.objectLockConfig, meta.VersioningConfigXML)
|
||||
// A lock configuration implies versioning: the bucket was created without
|
||||
// lock, so receiving the configuration turns plain Enabled versioning on.
|
||||
if meta.objectLockConfig == nil || !bytes.Equal(meta.VersioningConfigXML, enabledBucketVersioningConfig) {
|
||||
t.Fatalf("%s: bucket metadata = objectLock:%v versioning:%q", instanceType, meta.objectLockConfig, meta.VersioningConfigXML)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -69,12 +69,14 @@ func testSiteReplicationStatusAccountsPerSiteAndSurvivesMalformedConfig(obj Obje
|
||||
bucketSSEConfig: sseXML,
|
||||
bucketQuotaConfigFile: quotaJSON,
|
||||
bucketPolicyConfig: policyJSON,
|
||||
bucketCorsConfig: []byte(testSiteReplicationCORSDoc),
|
||||
} {
|
||||
if _, err := globalBucketMetadataSys.Update(ctx, localBucket, configFile, data); err != nil {
|
||||
t.Fatalf("%s: update %s: %v", instanceType, configFile, err)
|
||||
}
|
||||
}
|
||||
if _, err := updateLocalBucketCORSMetadata(ctx, obj, localBucket, []byte(testSiteReplicationCORSDoc)); err != nil {
|
||||
t.Fatalf("%s: update %s: %v", instanceType, bucketCorsConfig, err)
|
||||
}
|
||||
|
||||
encode := func(data []byte) *string {
|
||||
encoded := base64.StdEncoding.EncodeToString(data)
|
||||
|
||||
@@ -889,7 +889,11 @@ func (c *SiteReplicationSys) DeleteBucketHook(ctx context.Context, bucket string
|
||||
return errors.Unwrap(cerr)
|
||||
}
|
||||
|
||||
func enablePeerBucketVersioning(meta *BucketMetadata) error {
|
||||
// enablePeerBucketVersioning turns versioning on for a bucket that is being
|
||||
// created or adopted. With lockEnabled, Object Lock requires every object to
|
||||
// be versioned: the S3 API rejects suspended or prefix-excluded versioning on
|
||||
// a locked bucket, so such a configuration is replaced rather than preserved.
|
||||
func enablePeerBucketVersioning(meta *BucketMetadata, lockEnabled bool) error {
|
||||
if len(meta.VersioningConfigXML) == 0 {
|
||||
meta.VersioningConfigXML = enabledBucketVersioningConfig
|
||||
if meta.VersioningConfigUpdatedAt.IsZero() {
|
||||
@@ -898,7 +902,7 @@ func enablePeerBucketVersioning(meta *BucketMetadata) error {
|
||||
return nil
|
||||
}
|
||||
config, err := versioning.ParseConfig(bytes.NewReader(meta.VersioningConfigXML))
|
||||
if err != nil {
|
||||
if err != nil || (lockEnabled && (config.Suspended() || config.PrefixesExcluded())) {
|
||||
meta.VersioningConfigXML = enabledBucketVersioningConfig
|
||||
meta.VersioningConfigUpdatedAt = UTCNow()
|
||||
return nil
|
||||
@@ -943,7 +947,7 @@ func (c *SiteReplicationSys) PeerBucketMakeWithVersioningHandler(ctx context.Con
|
||||
}
|
||||
meta.SetCreatedAt(opts.CreatedAt)
|
||||
|
||||
if err = enablePeerBucketVersioning(&meta); err != nil {
|
||||
if err = enablePeerBucketVersioning(&meta, opts.LockEnabled || len(meta.ObjectLockConfigXML) != 0); err != nil {
|
||||
return err
|
||||
}
|
||||
if opts.LockEnabled && len(meta.ObjectLockConfigXML) == 0 {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# pgsty/minio Security Advisories
|
||||
# pgsty/silo Security Advisories
|
||||
|
||||
This document summarizes fork-specific security fixes and closely related upgrade-impacting security notes in `pgsty/minio`. It is intentionally narrower than a full changelog and focuses on release-impacting security behavior.
|
||||
This document summarizes fork-specific security fixes and closely related upgrade-impacting security notes in `pgsty/silo`. It is intentionally narrower than a full changelog and focuses on release-impacting security behavior.
|
||||
|
||||
Entries carry a CVE identifier where one exists. Where none does, they carry a fork-local `SN-<year>-<sequence>` identifier so that a finding without a CVE can still be referenced stably from release notes, commits and issues. An `SN-` identifier is **not** a CVE and is not registered in any vulnerability database; it is deliberately not written in CVE form so that scanners do not mistake it for one. Upstream `minio/minio` is archived, so for findings in inherited code there is no upstream maintainer to coordinate a CVE assignment with. `SN-2026-001` is the streaming-flush regression in `trackingResponseWriter`, which is a reliability defect rather than a security one and is tracked in the release notes rather than here.
|
||||
|
||||
@@ -24,10 +24,15 @@ The first Silo community release was cut from upstream history that already cont
|
||||
| [CVE-2026-40344](https://github.com/advisories/GHSA-9c4q-hq6p-c237) | `efb6e5b00` | Snowball auto-extract authentication | Yes | Verifies request authentication before tar extraction in Snowball unsigned-trailer flows | Upgrade if you use `PutObjectExtract` or Snowball uploads. |
|
||||
| [CVE-2026-42600](https://github.com/advisories/GHSA-xh8f-g2qw-gcm7) | `73ac52472` | Internode `ReadMultiple` storage-REST endpoint | Yes (cluster-root JWT required) | Removes the unused endpoint that allowed path traversal outside configured drive roots | Upgrade distributed-erasure deployments. Single-node deployments do not register this route. |
|
||||
| `SN-2026-002` | `ca7baa670` and follow-ups | Internode storage-REST and Grid RPC payloads | Yes (cluster-root / internode JWT required) | Completes CVE-2026-42600. Its fix removed one endpoint that exercised the gap; the gap itself -- request bodies and grid frames never reaching the validity middleware, and no containment in the storage layer -- remained across three further protocol surfaces. Closes path traversal on both the volume and path axes (including the peer-S3 bucket RPCs, which bypass the storage-REST wrapper entirely), an unrecoverable divide-by-zero that killed a node per RPC frame, metadata that reported truncated shards as intact, and three allocations sized from caller-declared values. | Upgrade distributed-erasure deployments. Single-node deployments register none of these routes. No S3 API behaviour changes; object keys containing `.` or `..` path segments were already refused at the S3 boundary. |
|
||||
| `SN-2026-003` | [`silo-pkg v3.11.0`](https://github.com/pgsty/silo-pkg/releases/tag/v3.11.0) and [`2f55347f7`](https://github.com/pgsty/minio/commit/2f55347f78352aed8e08866d370c9426c73362cf) | S3/IAM bucket-policy condition values | Yes (policy-dependent) | Prevents raw request entries that spell condition-key names from shadowing or synthesizing internal condition values; confines `s3:signatureAge` to verified SigV4 presigned requests; separates query-only list fields from header-backed `x-amz-*` fields; and stops client request tags from impersonating stored existing-object tags. | The compatible query form remains for storage class and upload tagging on handlers that consume it; an explicitly present header wins, including an empty header. The historical `X-Amz-Tagging` Header mapping remains a client-supplied `RequestObjectTag` source, so use request-tag conditions only on operations that consume tags. Header-only `x-amz-*` policy keys no longer accept query substitutes. `aws:SourceIp` was left following the existing forwarding-header trust model; that model is addressed separately in the next row. See [Condition value sources and precedence](https://silo.pgsty.com/administration/identity-access-management/policy-based-access-control/#condition-value-sources). |
|
||||
| `SN-2026-003` | [`silo-pkg v3.11.0`](https://github.com/pgsty/silo-pkg/releases/tag/v3.11.0) and [`2f55347f7`](https://github.com/pgsty/silo/commit/2f55347f78352aed8e08866d370c9426c73362cf) | S3/IAM bucket-policy condition values | Yes (policy-dependent) | Prevents raw request entries that spell condition-key names from shadowing or synthesizing internal condition values; confines `s3:signatureAge` to verified SigV4 presigned requests; separates query-only list fields from header-backed `x-amz-*` fields; and stops client request tags from impersonating stored existing-object tags. | The compatible query form remains for storage class and upload tagging on handlers that consume it; an explicitly present header wins, including an empty header. The historical `X-Amz-Tagging` Header mapping remains a client-supplied `RequestObjectTag` source, so use request-tag conditions only on operations that consume tags. Header-only `x-amz-*` policy keys no longer accept query substitutes. `aws:SourceIp` was left following the existing forwarding-header trust model; that model is addressed separately in the next row. See [Condition value sources and precedence](https://silo.pgsty.com/administration/identity-access-management/policy-based-access-control/#condition-value-sources). |
|
||||
| Not a vulnerability | `fe6dc4780` | Client source address (`aws:SourceIp`, audit `remotehost`, event notification `Host`) | N/A -- opt-in hardening | Adds an enforceable forwarded-header trust boundary, `MINIO_API_TRUSTED_PROXIES`. Set to a list of addresses or CIDR blocks, forwarded headers are believed only from those peers and forwarding chains are read right-to-left past listed hops -- which also stops the client-supplied left-most entry that an appending proxy (the stock nginx `$proxy_add_x_forwarded_for` recipe, or HAProxy's added second header line) leaves in place. Set to `none`, no forwarded header is believed at all. This is the guarantee `_MINIO_API_XFF_HEADER=off` never provided: it suppresses `X-Forwarded-For` alone, so `X-Real-IP` and RFC 7239 `Forwarded` remain one-line substitutions for anyone that setting was meant to stop. | **No behaviour change for any existing deployment**, so there is nothing to do on upgrade unless you want the new boundary. Not assigned a CVE: the default matches upstream, and upstream's own position (maintainer response in [discussion #17878](https://github.com/minio/minio/discussions/17878), Aug 2023) is that IP-based restrictions are impractical without reliable source-IP visibility. The gap being closed is that this was never written anywhere an operator would find it -- an `IpAddress` condition is accepted and behaves as though it works. **If you use `IpAddress` or `NotIpAddress` conditions, note that they were not enforceable before this change**, including behind a reverse proxy whose `X-Forwarded-For` recipe appends rather than overwrites. If you do not, the change affects only the accuracy of client addresses in logs. The new variable is opt-in and inert when unset; `_MINIO_API_XFF_HEADER` keeps its exact upstream semantics, and upstream's `TestXFFDisabled` is retained unmodified as the proof. An `IpAddress` condition remains unenforceable by default against a client with direct network access to the API port -- that is the condition the allowlist exists to fix, not a regression introduced here. When enabling the allowlist: it must name proxies, not the subnet they sit in, because entries are skipped while walking the chain, so a range that also covers clients lets those clients forge. Multi-node deployments must include their own node addresses, since MinIO forwards some requests between nodes and a client can force a hop through the `ListObjectsV2` continuation token; prefer the allowlist over `none` on a cluster for that reason. Loopback is always trusted as a peer so FTP and SFTP keep attributing their sessions. A malformed value stops startup, as does one that names no proxy at all (`","`) or one whose `env://` remote could not be read -- `env.Get` discards that error and yields an empty string, which would otherwise read as unset. Whitespace-only remains equivalent to unset. The policy is read after `MINIO_CONFIG_ENV_FILE` is loaded so environment-file deployments are covered; `_MINIO_API_XFF_HEADER` deliberately keeps upstream's earlier read timing, where a value written into an environment file is ignored. `MINIO_IDENTITY_LDAP_STS_TRUSTED_PROXIES` now shares the same list parser, but is behaviourally untouched: the extraction is pure code motion, verified identical to the previous implementation across every combination of 37 allowlist values and 21 peer addresses. See [Client source address trust](source-address-trust.md). |
|
||||
| `SN-2026-004` | [`silo-pkg v3.11.0`](https://github.com/pgsty/silo-pkg/releases/tag/v3.11.0) and [`97b7d2804`](https://github.com/pgsty/minio/commit/97b7d28040d109061c0a46a4c01bfc7800a97cc1) | IAM policy evaluation of bucket-level actions | Yes (policy-dependent) | Withholds twelve sensitive bucket-level writes from an object-only resource pattern. The IAM matcher appended a trailing slash for bucket-level requests (empty object name), so a resource of `arn:aws:s3:::bucket/*` matched `"bucket/"` and authorized bucket-level actions it was never meant to reach -- upstream [minio/minio#20449](https://github.com/minio/minio/issues/20449). The bucket-policy evaluation path never had the slash and was already reference-correct. | **This is an authorization tightening; read this row before upgrading if you write your own bucket-scoped policies.** Withheld from `bucket/*` on `Allow` statements only: `PutBucketPolicy`, `DeleteBucketPolicy`, `PutBucketObjectLockConfiguration`, `PutBucketVersioning`, `PutReplicationConfiguration`, `PutBucketLifecycle`, `DeleteBucket`, `ForceDeleteBucket`, `PutBucketCors`, `DeleteBucketCors`, `PutBucketQOS`, `PutInventoryConfiguration`. Membership was decided by one question -- does reaching this action give the caller something its object-scoped grant does not already give it? -- because the bug only fires when the statement already grants the bucket action, which in practice means `s3:*`, so the affected principal already holds full object CRUD. Only actions that hand out access to others, defeat a protection aimed at write-holders, act under server credentials, outlive the grant, or destroy the bucket entity qualify. **Deliberately not withheld, and asserted by test so re-adding one is a deliberate act**: `ListBucket`, `GetBucketLocation` and the read/list family, `PutBucketTagging`, `PutBucketEncryption`, `PutBucketNotification`, and `CreateBucket` -- so `mc ls`, SDK session setup and ordinary tenant self-service keep working through `bucket/*`. Breaking those is what got upstream's own full fix reverted. **What to change**: add the bare bucket ARN (`arn:aws:s3:::bucket`) alongside `arn:aws:s3:::bucket/*` in any statement that legitimately grants one of the twelve. Built-in canned policies are unaffected (all use `Resource: "*"`). `Deny` statements are untouched, so no bucket lock is ever weakened, and `NotResource` exclusions keep their full reach. The hardening is monotone by construction rather than by argument: the protected path requires **both** the bare and the historical `"bucket/"` form to match, an intersection with the historical decision -- without that, a fixed-width wildcard such as `mybucke?` would match `"mybucket"` while never having matched `"mybucket/"`, and the hardening would have granted a write the buggy matcher refused. `MINIO_API_LEGACY_BUCKET_RESOURCE_MATCH=on` restores the historical behaviour in full; it is read once at startup. Still deferred to a migration-gated release: the read/list family, a startup audit naming affected policies, and a self-explaining denial log. |
|
||||
| `SN-2026-004` | [`silo-pkg v3.11.0`](https://github.com/pgsty/silo-pkg/releases/tag/v3.11.0) and [`97b7d2804`](https://github.com/pgsty/silo/commit/97b7d28040d109061c0a46a4c01bfc7800a97cc1) | IAM policy evaluation of bucket-level actions | Yes (policy-dependent) | Withholds twelve sensitive bucket-level writes from an object-only resource pattern. The IAM matcher appended a trailing slash for bucket-level requests (empty object name), so a resource of `arn:aws:s3:::bucket/*` matched `"bucket/"` and authorized bucket-level actions it was never meant to reach -- upstream [minio/minio#20449](https://github.com/minio/minio/issues/20449). The bucket-policy evaluation path never had the slash and was already reference-correct. | **This is an authorization tightening; read this row before upgrading if you write your own bucket-scoped policies.** Withheld from `bucket/*` on `Allow` statements only: `PutBucketPolicy`, `DeleteBucketPolicy`, `PutBucketObjectLockConfiguration`, `PutBucketVersioning`, `PutReplicationConfiguration`, `PutBucketLifecycle`, `DeleteBucket`, `ForceDeleteBucket`, `PutBucketCors`, `DeleteBucketCors`, `PutBucketQOS`, `PutInventoryConfiguration`. Membership was decided by one question -- does reaching this action give the caller something its object-scoped grant does not already give it? -- because the bug only fires when the statement already grants the bucket action, which in practice means `s3:*`, so the affected principal already holds full object CRUD. Only actions that hand out access to others, defeat a protection aimed at write-holders, act under server credentials, outlive the grant, or destroy the bucket entity qualify. **Deliberately not withheld, and asserted by test so re-adding one is a deliberate act**: `ListBucket`, `GetBucketLocation` and the read/list family, `PutBucketTagging`, `PutBucketEncryption`, `PutBucketNotification`, and `CreateBucket` -- so `mc ls`, SDK session setup and ordinary tenant self-service keep working through `bucket/*`. Breaking those is what got upstream's own full fix reverted. **What to change**: add the bare bucket ARN (`arn:aws:s3:::bucket`) alongside `arn:aws:s3:::bucket/*` in any statement that legitimately grants one of the twelve. Built-in canned policies are unaffected (all use `Resource: "*"`). `Deny` statements are untouched, so no bucket lock is ever weakened, and `NotResource` exclusions keep their full reach. The hardening is monotone by construction rather than by argument: the protected path requires **both** the bare and the historical `"bucket/"` form to match, an intersection with the historical decision -- without that, a fixed-width wildcard such as `mybucke?` would match `"mybucket"` while never having matched `"mybucket/"`, and the hardening would have granted a write the buggy matcher refused. `MINIO_API_LEGACY_BUCKET_RESOURCE_MATCH=on` restores the historical behaviour in full; it is read once at startup. Still deferred to a migration-gated release: the read/list family, a startup audit naming affected policies, and a self-explaining denial log. |
|
||||
| `SN-2026-005` | [`silo-pkg v3.12.0`](https://github.com/pgsty/silo-pkg/releases/tag/v3.12.0) and [`eee05a17c`](https://github.com/pgsty/silo/commit/eee05a17c34a07cebb27220d12697be74c8bd617) | IAM named-policy and service-account policy writes | No direct remote exploit; policy-dependent | Rejects S3, S3 Tables, and KMS ARN namespace prefixes that name no resource, including their historical `*arn:...` serialization, in both `Resource` and `NotResource`. A resource-matching `Deny` using such a prefix could silently fail to deny, while an `Allow` with the prefix in `NotResource` could match far more broadly than intended. The guard applies when creating named policies and when creating or updating service-account session policies. | **This is an authorization tightening for new and updated policies.** Existing policies keep loading, matching, importing, and replicating with unchanged runtime behavior, but a policy containing one of these prefixes cannot be submitted unchanged; replace it with the intended concrete resource, or use an explicit wildcard such as `arn:aws:s3:::*` only when all resources are intended. Enabling the strict path also rejects an admin statement that combines `Resource` with `NotResource`, and rejects non-S3 resources on bucket-scoped admin actions. Here “bare ARN prefix” means a namespace with no resource after it (`arn:aws:s3:::`); it is distinct from the valid “bare bucket ARN” in `SN-2026-004` (`arn:aws:s3:::bucket`). IAM import, site-replication receive paths, stored-policy loading, and STS inline policies remain on the permissive compatibility path in this release. |
|
||||
| `SN-2026-006` | [`b73581b05`](https://github.com/pgsty/silo/commit/b73581b05) and [`c4fd97d0b`](https://github.com/pgsty/silo/commit/c4fd97d0b) ([#82](https://github.com/pgsty/silo/issues/82)) | SSE-C reads of zero-byte objects (`GetObject`, `HeadObject`, `CopyObject` source, `GetObjectAttributes`) | Yes; requires read access to the object | Zero-byte SSE-C objects never unsealed the customer-provided key, so a wrong key was accepted with `200` instead of `403`, and a copy or new version could be created under a key of the caller's choosing without knowing the current one. | Wrong keys now fail with `403 AccessDenied` as on AWS; correct keys behave as before and no client change is needed. Inherited from upstream; every earlier release is affected. |
|
||||
| `SN-2026-007` | [`474cd5801`](https://github.com/pgsty/silo/commit/474cd5801), [`74c97d005`](https://github.com/pgsty/silo/commit/74c97d005), [`21870fa2e`](https://github.com/pgsty/silo/commit/21870fa2e) ([#84](https://github.com/pgsty/silo/issues/84)) | `GetObjectAttributes` on SSE-C objects | Yes; requires read access to the object | Attributes of SSE-C objects were returned without authenticating the customer key, and a bare `X-Minio-Source-Replication-Request` header skipped the check entirely. | A wrong key returns `403`, a replication marker without the key returns `400`; replication peers holding `s3:ReplicateObject` are unaffected. Inherited from upstream. |
|
||||
| `SN-2026-008` | [PR #101](https://github.com/pgsty/silo/pull/101) ([`938603458`](https://github.com/pgsty/silo/commit/938603458) through [`04b097fd9`](https://github.com/pgsty/silo/commit/04b097fd9)) | Internal replication request headers such as `X-Minio-Source-Etag`, `X-Minio-Source-Mtime`, `X-Minio-Source-Replication-Request`, the replication SSE key headers, and `X-Amz-Bucket-Replication-Status` on object reads, writes, multipart uploads, deletes, Snowball extraction, and bucket events | Yes; any authenticated principal that can read or write the object | Completes CVE-2026-34204. The server still trusted these internal headers on presence in most handlers: any client could preserve arbitrary ETags and modification times, read SSE-C ciphertext without the key, inject replication checksums and Object Lock timestamps, suppress bucket notifications, and route deletes as replication deletes. | Replication semantics now require the exact marker value together with `s3:ReplicateObject` or `s3:ReplicateDelete`; other requests have these headers removed after signature verification and are processed as ordinary requests. Site replication service accounts and bucket-replication targets that already hold the replication permissions are unaffected. Inherited from upstream. |
|
||||
| `SN-2026-009` | [`58735ee38`](https://github.com/pgsty/silo/commit/58735ee38) and [`229fe2b3c`](https://github.com/pgsty/silo/commit/229fe2b3c) ([PR #73](https://github.com/pgsty/silo/pull/73)) | Admin `SetUserStatus` and `SetGroupStatus` | Yes; authenticated admin API | Status changes were authorized against `admin:EnableUser` / `admin:EnableGroup` regardless of the requested status, so a principal allowed only to enable could also disable, and vice versa. | Enable and disable now require the action matching the target status. Policies that grant only one of the pair lose the other operation; `admin:*` and the built-in `consoleAdmin` policy are unaffected. Inherited from upstream. |
|
||||
| `SN-2026-010` | [PR #104](https://github.com/pgsty/silo/pull/104) ([`75a6734e4`](https://github.com/pgsty/silo/commit/75a6734e4) through [`d2d47a41f`](https://github.com/pgsty/silo/commit/d2d47a41f), [#58](https://github.com/pgsty/silo/issues/58)) | `DeleteObject` and `DeleteObjects` with an explicit `versionId` | Yes; authenticated S3 API | Explicit version deletes were authorized as `s3:DeleteObject` with only a deny check on `s3:DeleteObjectVersion`, diverging from AWS. | Explicit version deletes now require `s3:DeleteObjectVersion`, as on AWS. **Two policy effects:** principals granted only `s3:DeleteObject` can no longer delete specific versions, and a policy that relied on `Deny s3:DeleteObject` to block permanent deletes must also deny `s3:DeleteObjectVersion`, because `Allow s3:*` now permits explicit version deletes. Replication targets keep the `s3:ReplicateDelete` contract. Inherited from upstream. |
|
||||
|
||||
## Dependency security updates
|
||||
|
||||
@@ -36,6 +41,7 @@ The first Silo community release was cut from upstream history that already cont
|
||||
| `CVE-2026-34986` | `68e0ba997` | Upgrades `go-jose` to `v4.1.4`. |
|
||||
| `CVE-2026-39883` | `1869bd30b`, `e4fa06394` | Updates OpenTelemetry dependencies. |
|
||||
| Upstream Go security fixes | [Go 1.26.5](https://go.dev/doc/devel/release#go1.26.5) | Bumps the required toolchain to Go 1.26.5, which includes security fixes to `crypto/tls` and `os`. |
|
||||
| Toolchain and dependency refresh | [Go 1.27.0](https://go.dev/doc/devel/release#go1.27) via [`43f4bb7ed`](https://github.com/pgsty/silo/commit/43f4bb7ed), [`edc8be6ed`](https://github.com/pgsty/silo/commit/edc8be6ed), [`4d6e1ea8e`](https://github.com/pgsty/silo/commit/4d6e1ea8e) | Moves the toolchain to Go 1.27.0 and refreshes the dependency stack (etcd client v3.7.1, `jwx` v3.0.13, `klauspost/compress` v1.19.2). The pre-release cleanup then returns to upstream `minio-go` (v7.3.1 pre-release) and retires the `silo-go` fork; `govulncheck` reports no reachable vulnerability on the release candidate. |
|
||||
| [GO-2026-6061](https://pkg.go.dev/vuln/GO-2026-6061) / [GHSA-hrxh-6v49-42gf](https://github.com/advisories/GHSA-hrxh-6v49-42gf) | gRPC `v1.82.1` | Updates gRPC to the first fixed version for vulnerabilities in the xDS RBAC authorization engine and HTTP/2 transport server. |
|
||||
| [GO-2026-5970](https://pkg.go.dev/vuln/GO-2026-5970) / `CVE-2026-56852` | `x/text` `v0.39.0` | Updates `x/text` to the first fixed version for an infinite loop on invalid input. |
|
||||
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
|
||||
## Status
|
||||
|
||||
- Issue: [pgsty/silo#75](https://github.com/pgsty/silo/issues/75)
|
||||
- Baseline: `e4e3007da6d7d1198a6a050e34f84566d40a9654`
|
||||
- Working branch: `codex/issue-75-cors-hardening`
|
||||
- Decision: CORS-specific deterministic last-writer-wins register, described below
|
||||
- Implementation state: B2 is commit `724f8703d`; the final B2+B3 integration is signed commit `0eebc928f` on the PR #80 branch and has passed combined local acceptance
|
||||
- Release state: PR #80 remains open; nothing is merged, tagged, packaged, published as an image, or deployed
|
||||
- Final design/implementation review: the B2 implementation was GO; the combined B2+B3 Opus 5 Max review found one test-build conflict and one legacy-metadata load risk, both corrected before combined testing
|
||||
- Issue: [pgsty/silo#75](https://github.com/pgsty/silo/issues/75), closed; follow-ups
|
||||
[#77](https://github.com/pgsty/silo/issues/77) and [#102](https://github.com/pgsty/silo/issues/102)
|
||||
- Merged: [PR #80](https://github.com/pgsty/silo/pull/80) implemented this register
|
||||
(2026-08-29); [PR #101](https://github.com/pgsty/silo/pull/101) restricted the
|
||||
pre-authentication lookup to resident metadata; [PR #103](https://github.com/pgsty/silo/pull/103)
|
||||
replaced the CORS-specific lock with the shared `metadata.lock`
|
||||
- Release state: on `main`, not yet in a tagged release as of 2026-09-02
|
||||
|
||||
This document defines the replication state, ordering, persistence, status,
|
||||
healing, concurrency, compatibility, and test contract for per-bucket CORS.
|
||||
@@ -501,92 +501,3 @@ The required test matrix is:
|
||||
| Restart | cache removal/disk reload preserves tombstone or live state and status timestamp |
|
||||
| Legacy repair | a lenient historical document loads fail-closed without hiding other metadata and can be deleted or replaced |
|
||||
| Full seam | signed admin dispatch -> peer apply -> real status collection -> local heal -> cache reload -> remote heal dispatch |
|
||||
|
||||
## Local Verification Record
|
||||
|
||||
The committed B2 implementation passed:
|
||||
|
||||
- the supplied adversarial base64 and same-payload/newer-timestamp tests;
|
||||
- focused CORS normal tests;
|
||||
- focused CORS race tests;
|
||||
- `go test ./internal/bucket/cors` and its race run;
|
||||
- `go test ./cmd -count=1`;
|
||||
- `go vet ./...`;
|
||||
- `go build ./...`;
|
||||
- repository-configured golangci-lint v2.13.1 with zero issues;
|
||||
- gofmt and `git diff --check`;
|
||||
- a signed admin dispatch -> apply -> status -> heal -> cache reload test.
|
||||
|
||||
After integrating B3 and resolving overlap, the frozen combination passed:
|
||||
|
||||
- focused strict-parser, validation, middleware, replication, namespace,
|
||||
legacy-repair, and race tests;
|
||||
- CI-tagged `go test ./...`, full vet/build, module verification, pinned lint,
|
||||
and rebrand/compatibility checks;
|
||||
- a real local two-site deployment with two nodes per site, including
|
||||
bidirectional replacement, a site missing DELETE while offline, restart
|
||||
heal, and a second restart preserving the tombstone; and
|
||||
- raw SigV4 wire probes that reject a lowercase method and trailing XML root,
|
||||
accept a 255-code-point Unicode ID, and replicate the accepted config.
|
||||
|
||||
The public EN/ZH design records pass a warning-fatal Hugo build, rendered link
|
||||
checking, and local browser QA. These results are acceptance evidence, not a
|
||||
release, deployment, tag, or production claim.
|
||||
|
||||
The repository `make lint` bootstrap could not download its private copy of
|
||||
golangci-lint because the network returned HTTP status 000. The same exact
|
||||
v2.13.1 binary already installed locally was used with the Makefile's build
|
||||
tags, timeout, and configuration and reported zero issues.
|
||||
|
||||
## Independent Review Record
|
||||
|
||||
Four read-only local Claude Code reviews used canonical model
|
||||
`claude-opus-5` at `max` effort.
|
||||
|
||||
The first review rejected the pre-fix candidate and identified the unsafe
|
||||
CreatedAt-based baseline, missing deterministic tie-break, missing atomic join,
|
||||
non-monotonic local barrier, timestamp-blind status, and initial-sync tombstone
|
||||
gap. The selected C-prime model incorporated the valid findings while rejecting
|
||||
the suggestion to rewrite normal source timestamps.
|
||||
|
||||
The second review found no P0. Its `GO WITH FIXES` findings were peer semantic
|
||||
validation, the legacy/default admin mutation path bypassing the then-current
|
||||
CORS lock and join, CreatedAt-floor observability, and missing tests for
|
||||
invalid XML, lineage, and concurrent local transitions. Those required changes
|
||||
and tests are now in the working tree.
|
||||
|
||||
The final review examined this design and the exact dirty diff, independently
|
||||
reran build, vet, lint, normal tests, and race tests, and found no P0 or P1.
|
||||
Its verdict was `GO WITH FIXES`: the implementation was explicitly judged GO,
|
||||
while five design-document statements required correction. It also suggested
|
||||
an optional status hardening so semantically invalid canonical payloads are
|
||||
not selected and retransmitted. The hardening and all mandatory documentation
|
||||
corrections are incorporated in the current tree. The final selected solution
|
||||
is therefore the C-prime register and invariants recorded in this document.
|
||||
|
||||
The fourth review examined the resolved B2+B3 combination. It confirmed that
|
||||
the C-prime register, strict wire parser, MaxAge presence, wildcard credentials,
|
||||
Origin-null marker, rejected-preflight `Vary`, checksum classification, and
|
||||
peer validation can coexist. It found a conflict-resolution test helper typo
|
||||
and the risk that strict parsing could make all bucket metadata unavailable for
|
||||
a document accepted by a lenient development build. The helper was corrected;
|
||||
metadata loading now stashes a CORS-specific error, fails browser behavior
|
||||
closed, rejects new invalid saves, and allows a valid CORS PUT/DELETE repair.
|
||||
|
||||
## Release Gates
|
||||
|
||||
An implementation-level GO means only that the local CORS state machine and
|
||||
tests satisfy this document. It does not authorize a release.
|
||||
|
||||
Before closing issue #75 or publishing a server artifact:
|
||||
|
||||
1. commit the exact reviewed implementation and design with DCO sign-off;
|
||||
2. push a focused branch and run remote PR CI;
|
||||
3. merge and confirm main CI on the merge commit;
|
||||
4. finish public EN/ZH upgrade, fallback, and downgrade documentation in
|
||||
`silo.pgsty.com`;
|
||||
5. run a real two-site process test for PUT, DELETE, simultaneous conflict,
|
||||
offline peer restart, status, and heal;
|
||||
6. verify no release tag or image contains an intermediate candidate; and
|
||||
7. treat package, image, SBOM, signature, canary, and production verification
|
||||
as separate gates.
|
||||
|
||||
@@ -2,22 +2,21 @@ module github.com/minio/minio
|
||||
|
||||
go 1.27.0
|
||||
|
||||
// Use PGSTY's maintained Silo Go SDK while preserving upstream import paths.
|
||||
// Keep the required version on a real upstream tag because replace directives
|
||||
// are ignored when this module is consumed as a dependency.
|
||||
replace github.com/minio/minio-go/v7 => github.com/pgsty/silo-go/v7 v7.3.1
|
||||
|
||||
// Use Pigsty's SILO Console v2.2.1 release while preserving upstream import paths.
|
||||
// The pseudo-version pins v2.2.1's commit because the compatible module path has no /v2 suffix.
|
||||
replace github.com/minio/console => github.com/pgsty/silo-console v0.0.0-20260829111139-e07ef01ab8bf
|
||||
// Use Pigsty's SILO Console while preserving upstream import paths. The
|
||||
// pseudo-version pins the last commit of the v2.3.0 line before Console moved
|
||||
// to the github.com/pgsty/silo-pkg/v3 module path: it carries the v2.3.0
|
||||
// security fixes and still consumes silo-pkg through the replacement below.
|
||||
replace github.com/minio/console => github.com/pgsty/silo-console v0.0.0-20260901090952-43f8447fda38
|
||||
|
||||
// Use Pigsty's maintained mc fork for Console's embedded client code.
|
||||
replace github.com/minio/mc => github.com/pgsty/mc v0.0.0-20260829103737-5ed037ef4ec1
|
||||
|
||||
// Use Pigsty's maintained SILO package fork while preserving upstream import paths.
|
||||
// This retains the LDAP TLS fix tracked in https://github.com/pgsty/silo/issues/15.
|
||||
// v3.12.2 retains the minio/minio#20449 bucket-write boundary hardening,
|
||||
// rejects bare ARN prefixes on strict policy-write paths, and selects Silo Go v7.3.1.
|
||||
// This retains the LDAP TLS fix tracked in https://github.com/pgsty/silo/issues/15,
|
||||
// the minio/minio#20449 bucket-write boundary hardening, bare ARN rejection on
|
||||
// strict policy-write paths. It is the last silo-pkg commit
|
||||
// that declares the github.com/minio/pkg/v3 module path; v3.13.0 moved to
|
||||
// github.com/pgsty/silo-pkg/v3 and cannot be selected through this replace.
|
||||
replace github.com/minio/pkg/v3 => github.com/pgsty/silo-pkg/v3 v3.12.3-0.20260829103855-748c94bf8ab7
|
||||
|
||||
// v22.7.0 does not compile on NetBSD because its unix implementation uses
|
||||
@@ -80,7 +79,7 @@ require (
|
||||
github.com/minio/kms-go/kes v0.3.1
|
||||
github.com/minio/kms-go/kms v0.6.0
|
||||
github.com/minio/madmin-go/v3 v3.0.110
|
||||
github.com/minio/minio-go/v7 v7.3.0
|
||||
github.com/minio/minio-go/v7 v7.3.1-0.20260828014306-0e78d3f18efe
|
||||
github.com/minio/mux v1.9.2
|
||||
github.com/minio/pkg/v3 v3.6.1
|
||||
github.com/minio/selfupdate v0.6.0
|
||||
|
||||
@@ -474,6 +474,8 @@ github.com/minio/madmin-go/v3 v3.0.110 h1:FIYekj7YPc430ffpXFWiUtyut3qBt/unIAcDzJ
|
||||
github.com/minio/madmin-go/v3 v3.0.110/go.mod h1:WOe2kYmYl1OIlY2DSRHVQ8j1v4OItARQ6jGyQqcCud8=
|
||||
github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34=
|
||||
github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM=
|
||||
github.com/minio/minio-go/v7 v7.3.1-0.20260828014306-0e78d3f18efe h1:By2FKNSOUGLOeb0x4D7xJMHr8x/X1ZW8PG780SpKUwQ=
|
||||
github.com/minio/minio-go/v7 v7.3.1-0.20260828014306-0e78d3f18efe/go.mod h1:KUPWdecEO1LWyUz+sTGXAuf2jZHrPh5fCsRH86QbPfk=
|
||||
github.com/minio/mux v1.9.2 h1:dQchne49BUBgOlxIHjx5wVe1gl5VXF2sxd4YCXkikTw=
|
||||
github.com/minio/mux v1.9.2/go.mod h1:OuHAsZsux+e562bcO2P3Zv/P0LMo6fPQ310SmoyG7mQ=
|
||||
github.com/minio/selfupdate v0.6.0 h1:i76PgT0K5xO9+hjzKcacQtO7+MjJ4JKA8Ak8XQ9DDwU=
|
||||
@@ -543,10 +545,8 @@ github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwp
|
||||
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
||||
github.com/pgsty/mc v0.0.0-20260829103737-5ed037ef4ec1 h1:CotSAPr11rZy3VK46j5rJQYQ2LHT2mjrd9vf0Ak12bA=
|
||||
github.com/pgsty/mc v0.0.0-20260829103737-5ed037ef4ec1/go.mod h1:pwkfUxTikOkKc+yvEWqFg7CAPZDj5Yl0YHFcZoPKmU8=
|
||||
github.com/pgsty/silo-console v0.0.0-20260829111139-e07ef01ab8bf h1:BwUlE3Qr5+/JWkdNYnpSFin7RW1Uiaf7ddGlR87Y9YQ=
|
||||
github.com/pgsty/silo-console v0.0.0-20260829111139-e07ef01ab8bf/go.mod h1:Zqpx0h9NB5WzXI+OE/VVFcrXD6fKtSqqfCurmtxyKPo=
|
||||
github.com/pgsty/silo-go/v7 v7.3.1 h1:CchXB5hdv1KGUCGBzR5Tz8hSE+3P9sGJFPY+ku8tC04=
|
||||
github.com/pgsty/silo-go/v7 v7.3.1/go.mod h1:3uUcXVLE1xLBTy+A/js/hHKkvcYQ/zMbj8rbtNQavxE=
|
||||
github.com/pgsty/silo-console v0.0.0-20260901090952-43f8447fda38 h1:PObL76LBUBKq+y9TPAmc8wlKD8C/BNyrXGjfUrWtUvk=
|
||||
github.com/pgsty/silo-console v0.0.0-20260901090952-43f8447fda38/go.mod h1:NHgq4XTLPa+Zt9lAIouHzgiFMULcyYjC3VwcMSuZf8U=
|
||||
github.com/pgsty/silo-pkg/v3 v3.12.3-0.20260829103855-748c94bf8ab7 h1:YSCnSqNjU2ddU84dA8ybJXOoWYx8VDu2qAzXMVweisc=
|
||||
github.com/pgsty/silo-pkg/v3 v3.12.3-0.20260829103855-748c94bf8ab7/go.mod h1:hSvfIz9FWTvEZqTsGEWQ8P/gD81pDee9W4j5oIvHZMY=
|
||||
github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM=
|
||||
|
||||
@@ -290,12 +290,6 @@ func (r Rule) matchAllowedOrigin(origin string) (string, bool) {
|
||||
return "", false
|
||||
}
|
||||
|
||||
// HasAllowedOrigin reports whether the rule allows the given origin.
|
||||
func (r Rule) HasAllowedOrigin(origin string) bool {
|
||||
_, ok := r.matchAllowedOrigin(origin)
|
||||
return ok
|
||||
}
|
||||
|
||||
// HasAllowedMethod reports whether the rule allows the given HTTP method.
|
||||
func (r Rule) HasAllowedMethod(method string) bool {
|
||||
for _, m := range r.AllowedMethods {
|
||||
|
||||
@@ -72,11 +72,6 @@ const (
|
||||
EnvMySQLFormat = "MINIO_NOTIFY_MYSQL_FORMAT"
|
||||
EnvMySQLDSNString = "MINIO_NOTIFY_MYSQL_DSN_STRING"
|
||||
EnvMySQLTable = "MINIO_NOTIFY_MYSQL_TABLE"
|
||||
EnvMySQLHost = "MINIO_NOTIFY_MYSQL_HOST"
|
||||
EnvMySQLPort = "MINIO_NOTIFY_MYSQL_PORT"
|
||||
EnvMySQLUsername = "MINIO_NOTIFY_MYSQL_USERNAME"
|
||||
EnvMySQLPassword = "MINIO_NOTIFY_MYSQL_PASSWORD"
|
||||
EnvMySQLDatabase = "MINIO_NOTIFY_MYSQL_DATABASE"
|
||||
EnvMySQLQueueLimit = "MINIO_NOTIFY_MYSQL_QUEUE_LIMIT"
|
||||
EnvMySQLQueueDir = "MINIO_NOTIFY_MYSQL_QUEUE_DIR"
|
||||
EnvMySQLMaxOpenConnections = "MINIO_NOTIFY_MYSQL_MAX_OPEN_CONNECTIONS"
|
||||
|
||||
@@ -69,11 +69,6 @@ const (
|
||||
EnvPostgresFormat = "MINIO_NOTIFY_POSTGRES_FORMAT"
|
||||
EnvPostgresConnectionString = "MINIO_NOTIFY_POSTGRES_CONNECTION_STRING"
|
||||
EnvPostgresTable = "MINIO_NOTIFY_POSTGRES_TABLE"
|
||||
EnvPostgresHost = "MINIO_NOTIFY_POSTGRES_HOST"
|
||||
EnvPostgresPort = "MINIO_NOTIFY_POSTGRES_PORT"
|
||||
EnvPostgresUsername = "MINIO_NOTIFY_POSTGRES_USERNAME"
|
||||
EnvPostgresPassword = "MINIO_NOTIFY_POSTGRES_PASSWORD"
|
||||
EnvPostgresDatabase = "MINIO_NOTIFY_POSTGRES_DATABASE"
|
||||
EnvPostgresQueueDir = "MINIO_NOTIFY_POSTGRES_QUEUE_DIR"
|
||||
EnvPostgresQueueLimit = "MINIO_NOTIFY_POSTGRES_QUEUE_LIMIT"
|
||||
EnvPostgresMaxOpenConnections = "MINIO_NOTIFY_POSTGRES_MAX_OPEN_CONNECTIONS"
|
||||
|
||||
@@ -57,11 +57,7 @@ func WaitPipe() (*PipeReader, *PipeWriter) {
|
||||
r, w := io.Pipe()
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(1)
|
||||
return &PipeReader{
|
||||
PipeReader: r,
|
||||
wait: wg.Wait,
|
||||
}, &PipeWriter{
|
||||
PipeWriter: w,
|
||||
done: wg.Done,
|
||||
}
|
||||
pr := &PipeReader{PipeReader: r, wait: wg.Wait}
|
||||
pw := &PipeWriter{PipeWriter: w, done: wg.Done}
|
||||
return pr, pw
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user