diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md deleted file mode 100644 index 8ee69c852..000000000 --- a/.github/ISSUE_TEMPLATE.md +++ /dev/null @@ -1,54 +0,0 @@ ---- -name: Bug report -about: Create a report to help us improve -title: '' -labels: community, triage -assignees: '' - ---- - -## NOTE - -Silo issues are handled by community maintainers on a best-effort basis. There -is no SLA, SLO, or emergency production-support channel. Follow the local -[Code of Conduct](../code_of_conduct.md) when participating. Report suspected -vulnerabilities through the private process in [SECURITY.md](../SECURITY.md), -not in a public issue. - - - -## Expected Behavior - - - -## Current Behavior - - - -## Possible Solution - - - -## Steps to Reproduce (for bugs) - - - - -1. -2. -3. -4. - -## Context - - - -## Regression - - - -## Your Environment - -* Version used (`silo --version`): -* Server setup and configuration: -* Operating System and version (`uname -a`): diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 29c5ae3c9..608a4ae84 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -7,6 +7,14 @@ assignees: '' --- +Report bugs in the PGSTY SILO server (`pgsty/silo`) here. Community maintainers +handle reports on a best-effort basis. There is no SLA, SLO, or emergency +production-support channel. Follow the +[Code of Conduct](https://github.com/pgsty/silo/blob/main/code_of_conduct.md). +Report suspected vulnerabilities privately through +[SECURITY.md](https://github.com/pgsty/silo/blob/main/SECURITY.md). +For patches, see the [contribution guide](https://github.com/pgsty/silo/blob/main/CONTRIBUTING.md). + ## Expected Behavior diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index 4a7b218c9..d52c07177 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -7,6 +7,9 @@ assignees: '' --- +Suggest improvements to the PGSTY SILO server (`pgsty/silo`) here. +For patches, see the [contribution guide](https://github.com/pgsty/silo/blob/main/CONTRIBUTING.md). + **Is your feature request related to a problem? Please describe.** A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 00ffb2e0d..25fcbf59f 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,10 +1,14 @@ ## Contribution Licensing (no CLA, inbound=outbound, DCO required) -This project does not use a CLA; contributions are accepted inbound=outbound. +This pull request contributes to PGSTY SILO (`pgsty/silo`). Code contributions +are accepted under AGPL-3.0-or-later, the same license as the server. +This project does not use a CLA or require a separate Apache-2.0 license grant. By submitting this pull request I represent that I have the right to contribute -the changes, which are licensed under this repository's +the code changes under this repository's [GNU Affero General Public License v3.0 or later](https://www.gnu.org/licenses/agpl-3.0.html) -and remain my copyright. Every commit must carry a DCO `Signed-off-by` trailer +and retain copyright in my original work. Existing copyright and license +notices remain intact; separately licensed material keeps its applicable terms. +Every commit must carry a DCO `Signed-off-by` trailer (`git commit -s`) certifying the [Developer Certificate of Origin](https://developercertificate.org/) — see [CONTRIBUTING.md](https://github.com/pgsty/silo/blob/main/CONTRIBUTING.md). diff --git a/.github/goreleaser.yml b/.github/goreleaser.yml index 6c0ae33e8..c36fae2d4 100644 --- a/.github/goreleaser.yml +++ b/.github/goreleaser.yml @@ -77,8 +77,10 @@ release: name: silo draft: true prerelease: false - mode: append + replace_existing_draft: true replace_existing_artifacts: false + mode: replace + # Draft replacement matches the release name; keep it identical to the tag. name_template: "{{ .Tag }}" changelog: diff --git a/.github/workflows/docker-release.yml b/.github/workflows/docker-release.yml index 0106fb856..0bc860d8b 100644 --- a/.github/workflows/docker-release.yml +++ b/.github/workflows/docker-release.yml @@ -7,6 +7,11 @@ on: description: "Published RELEASE.* tag to package as pgsty/silo" required: true type: string + recovery: + description: "Run the current main workflow against an already-published tag" + required: false + default: false + type: boolean permissions: contents: read @@ -75,12 +80,18 @@ jobs: fetch-depth: 0 - name: Verify workflow identity matches release source + env: + DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} + RECOVERY: ${{ inputs.recovery }} run: | set -euo pipefail CHECKED_OUT_REVISION="$(git rev-parse HEAD)" if [ "${CHECKED_OUT_REVISION}" != "${GITHUB_SHA}" ]; then - echo "Checked out ${CHECKED_OUT_REVISION}, but workflow identity is ${GITHUB_SHA}. Dispatch this workflow from ${RELEASE_TAG}." >&2 - exit 1 + if [ "${RECOVERY}" != "true" ] || [ "${GITHUB_REF}" != "refs/heads/${DEFAULT_BRANCH}" ]; then + echo "Checked out ${CHECKED_OUT_REVISION}, but workflow identity is ${GITHUB_SHA}. Dispatch from ${RELEASE_TAG}, or use recovery from ${DEFAULT_BRANCH}." >&2 + exit 1 + fi + echo "Recovery workflow ${GITHUB_SHA} is packaging published source ${CHECKED_OUT_REVISION}." fi - name: Prepare verified Docker contexts @@ -133,6 +144,46 @@ jobs: "${context}/dockerscripts/" done + # The classic image bundles mcli. Resolve its two archive digests + # from the immutable published release instead of trusting defaults + # copied into an older Server tag. This also gives a recovery run a + # narrow override when a tag selected the right mcli release but + # accidentally retained stale archive pins. + MC_REPO="$(awk -F= '/^ARG MC_REPO=/{print $2; exit}' Dockerfile.goreleaser)" + MC_VERSION="$(awk -F= '/^ARG MC_VERSION=/{print $2; exit}' Dockerfile.goreleaser)" + test -n "${MC_REPO}" + test -n "${MC_VERSION}" + MC_VERSION_HYPHEN="${MC_VERSION#RELEASE.}" + MC_PKG_VERSION="$(echo "${MC_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 [ "${MC_PKG_VERSION}" = "${MC_VERSION_HYPHEN}" ]; then + echo "Invalid bundled mcli tag: ${MC_VERSION}" >&2 + exit 1 + fi + if [ "$(gh release view "${MC_VERSION}" --repo "${MC_REPO}" --json isDraft --jq .isDraft)" != false ] || \ + [ "$(gh release view "${MC_VERSION}" --repo "${MC_REPO}" --json isPrerelease --jq .isPrerelease)" != false ] || \ + [ "$(gh release view "${MC_VERSION}" --repo "${MC_REPO}" --json isImmutable --jq .isImmutable)" != true ]; then + echo "Bundled mcli ${MC_REPO}@${MC_VERSION} must be a published immutable release" >&2 + exit 1 + fi + + mc_checksums="mcli_${MC_PKG_VERSION}_checksums.txt" + gh release download "${MC_VERSION}" --repo "${MC_REPO}" \ + --dir "${assets_dir}" --pattern "${mc_checksums}" + gh attestation verify "${assets_dir}/${mc_checksums}" \ + --repo "${MC_REPO}" \ + --signer-workflow "${MC_REPO}/.github/workflows/release.yml" \ + --source-ref "refs/tags/${MC_VERSION}" >/dev/null + + MC_AMD64_SHA256="$(awk -v name="mcli_${MC_PKG_VERSION}_linux_amd64.tar.gz" '$2 == name {print $1}' "${assets_dir}/${mc_checksums}")" + MC_ARM64_SHA256="$(awk -v name="mcli_${MC_PKG_VERSION}_linux_arm64.tar.gz" '$2 == name {print $1}' "${assets_dir}/${mc_checksums}")" + [[ "${MC_AMD64_SHA256}" =~ ^[0-9a-f]{64}$ ]] + [[ "${MC_ARM64_SHA256}" =~ ^[0-9a-f]{64}$ ]] + + { + echo "MC_AMD64_SHA256=${MC_AMD64_SHA256}" + echo "MC_ARM64_SHA256=${MC_ARM64_SHA256}" + } >> "${GITHUB_ENV}" + echo "RELEASE_REVISION=$(git rev-parse HEAD)" >> "${GITHUB_ENV}" - name: Set up QEMU @@ -162,6 +213,9 @@ jobs: file: docker-release/amd64/Dockerfile.goreleaser platforms: linux/amd64 push: true + build-args: | + MC_AMD64_SHA256=${{ env.MC_AMD64_SHA256 }} + MC_ARM64_SHA256=${{ env.MC_ARM64_SHA256 }} tags: | pgsty/silo:${{ env.RELEASE_TAG }}-amd64 pgsty/silo:latest-amd64 @@ -178,6 +232,9 @@ jobs: file: docker-release/arm64/Dockerfile.goreleaser platforms: linux/arm64 push: true + build-args: | + MC_AMD64_SHA256=${{ env.MC_AMD64_SHA256 }} + MC_ARM64_SHA256=${{ env.MC_ARM64_SHA256 }} tags: | pgsty/silo:${{ env.RELEASE_TAG }}-arm64 pgsty/silo:latest-arm64 diff --git a/.github/workflows/finalize-release.yml b/.github/workflows/finalize-release.yml index 92f63d558..5f4950ed3 100644 --- a/.github/workflows/finalize-release.yml +++ b/.github/workflows/finalize-release.yml @@ -23,7 +23,7 @@ permissions: artifact-metadata: write concurrency: - group: finalize-release + group: release-${{ inputs.tag }} cancel-in-progress: false jobs: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c4b9aaae0..e3350283a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,5 +1,10 @@ name: Release +# Retry contract: an absent or single unfinalized Draft may be rebuilt from +# scratch; a published release or a Draft carrying finalize's GPG-derived +# provenance marker is terminal for this lane. The per-tag lock serializes +# workflows, but a maintainer must not publish the Draft while this job runs. + on: push: tags: @@ -16,6 +21,10 @@ permissions: attestations: write artifact-metadata: write +concurrency: + group: release-${{ github.event_name == 'workflow_dispatch' && inputs.tag || github.ref_name }} + cancel-in-progress: false + jobs: release: runs-on: ubuntu-latest @@ -78,6 +87,15 @@ jobs: echo "Invalid release tag format: ${TAG}" >&2 exit 1 fi + if ! TAG_COMMIT="$(git rev-parse "${TAG}^{commit}" 2>/dev/null)"; then + echo "Release tag ${TAG} does not resolve to a commit" >&2 + exit 1 + fi + HEAD_COMMIT="$(git rev-parse HEAD)" + if [ "${TAG_COMMIT}" != "${HEAD_COMMIT}" ]; then + echo "Release tag ${TAG} resolves to ${TAG_COMMIT}, checkout is ${HEAD_COMMIT}" >&2 + exit 1 + fi VERSION_HYPHEN="${TAG#RELEASE.}" PKG_VERSION="$(echo "${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/')" VERSION_COLON="$(echo "${VERSION_HYPHEN}" | sed -E 's/T([0-9]{2})-([0-9]{2})-([0-9]{2})Z$/T\1:\2:\3Z/')" @@ -93,6 +111,13 @@ jobs: echo "Package version: ${PKG_VERSION}" echo "LDFLAGS: ${LDFLAGS}" + - name: Check existing release state + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + set -euo pipefail + buildscripts/check-release-state.sh "${RELEASE_TAG}" + # Both installer actions are pinned to immutable commits. The explicit # tool versions keep the release format reproducible across workflow # reruns while the installers verify the downloaded executables. @@ -113,6 +138,7 @@ jobs: args: release --clean --skip=validate --config .github/goreleaser.yml env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GORELEASER_CURRENT_TAG: ${{ env.RELEASE_TAG }} LDFLAGS: ${{ env.LDFLAGS }} PKG_VERSION: ${{ env.PKG_VERSION }} @@ -189,6 +215,14 @@ jobs: test -s "${BUNDLE_PATH}" cp "${BUNDLE_PATH}" "dist/silo_${PKG_VERSION}_provenance.sigstore.json" + - name: Confirm unfinalized Draft release state + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + REQUIRE_DRAFT: "true" + run: | + set -euo pipefail + buildscripts/check-release-state.sh "${RELEASE_TAG}" + - name: Upload nFPM packages to Draft release env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/test-release.yml b/.github/workflows/test-release.yml index e717fcde6..2ab3c2ad3 100644 --- a/.github/workflows/test-release.yml +++ b/.github/workflows/test-release.yml @@ -23,6 +23,8 @@ on: - "buildscripts/minio-upgrade.sh" - "buildscripts/sign-release-rpms.sh" - "buildscripts/verify-build-provenance.sh" + - "buildscripts/check-release-state.sh" + - "buildscripts/check-release-state_test.sh" - "buildscripts/verify-rebrand.sh" - "buildscripts/verify-helm-migration.sh" - "buildscripts/helm-migration-guard/**" @@ -479,6 +481,8 @@ jobs: bash -n buildscripts/minio-upgrade.sh bash -n buildscripts/sign-release-rpms.sh bash -n buildscripts/verify-build-provenance.sh + bash -n buildscripts/check-release-state.sh + bash -n buildscripts/check-release-state_test.sh bash -n buildscripts/verify-rebrand.sh bash -n buildscripts/verify-helm-migration.sh sh -n buildscripts/package/postinstall.sh @@ -493,9 +497,12 @@ jobs: test -x buildscripts/package-release.sh test -x buildscripts/sign-release-rpms.sh test -x buildscripts/verify-build-provenance.sh + test -x buildscripts/check-release-state.sh + test -x buildscripts/check-release-state_test.sh test -x buildscripts/verify-rebrand.sh test -x buildscripts/verify-helm-migration.sh test -x buildscripts/package/postinstall.sh test -x buildscripts/package/preremove.sh test -x buildscripts/package/lifecycle_test.sh test -x dockerscripts/docker-entrypoint_test.sh + buildscripts/check-release-state_test.sh diff --git a/.github/workflows/vulncheck.yml b/.github/workflows/vulncheck.yml index 87a4aa7e9..63fc1c634 100644 --- a/.github/workflows/vulncheck.yml +++ b/.github/workflows/vulncheck.yml @@ -29,7 +29,7 @@ jobs: - name: Install govulncheck run: | - go install golang.org/x/vuln/cmd/govulncheck@v1.6.0 + go install golang.org/x/vuln/cmd/govulncheck@v1.7.0 echo "$(go env GOPATH)/bin" >> "${GITHUB_PATH}" - name: Run govulncheck diff --git a/.golangci.yml b/.golangci.yml index 0533d7cd9..90ce14bf4 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -5,7 +5,7 @@ linters: - durationcheck - forcetypeassert - gocritic - - gomodguard + - gomodguard_v2 - govet - ineffassign - misspell diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 93f8d1695..957fb787c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -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/*`, @@ -75,9 +79,10 @@ documentation is owned by the separate ## Licensing of Contributions -Silo is licensed under the [GNU AGPL v3.0 or later](LICENSE). Its core is -Copyright (c) MinIO, Inc.; the combined work can never be relicensed, and this -fork does not try to. +Code contributions to PGSTY SILO (`pgsty/silo`) are accepted under the +[GNU AGPL v3.0 or later](LICENSE), the same license as the server. Submit issues +and pull requests to this repository's maintainers. No separate Apache-2.0 +license grant to SILO or upstream MinIO maintainers is required. * **No CLA.** We do not ask you to sign a Contributor License Agreement and we do not take your copyright. Contributions are accepted inbound=outbound: you @@ -108,15 +113,20 @@ fork does not try to. `Signed-off-by` trailers) and add your own sign-off as the person passing it along. Never import code from a proprietary distribution. -* **File headers.** Files derived from upstream keep the original MinIO - copyright header unchanged. New files added by this fork use the dual - header, followed by the standard AGPL boilerplate: +* **File headers.** Preserve existing copyright and license notices in inherited + and third-party files. New original files name their actual copyright holders + and use AGPL-3.0-or-later. Use a header such as the following, then append the + standard AGPL boilerplate: ``` - // Copyright (c) 2015-2025 MinIO, Inc. - // Copyright (c) 2025-2026 PGSTY + // Copyright (c) 2026 Your Name ``` +* **Separately licensed material.** Documentation contributions in `docs/` + follow its existing [CC BY 4.0 license](docs/LICENSE). Third-party components + and earlier Apache-2.0 contributions retain their original licenses and + attribution; this policy does not relicense earlier work. + * **Squash merges** must keep the `Signed-off-by:` trailers in the resulting commit message. diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 4367c581b..93cfc1ed0 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -1,77 +1,182 @@ # Contributors -Silo is maintained by the Pigsty community. This file records the people who have contributed to -this fork since it was established in 2026 — merged code, proposed changes, and the bug reports and -compatibility findings that shaped the releases. +SILO is built by **40 community contributors**, including its maintainers. This record covers every +human author who has opened an issue or pull request in the repositories listed below, in any state. +It was checked against the complete, paginated GitHub API records on **2026-09-07 03:00:14 UTC**. -It is maintained by hand and updated at each release. GitHub's own contributor graph is unavailable -here because `pgsty/silo` is a fork, so this file — not that page — is the project's attribution -record. Contributors are listed by GitHub handle. Authorship of every merged commit is preserved in -the Git history and can be verified with `git log --format='%an <%ae>'`. +Contributors appear once in the avatar wall: authors of merged PRs first, other PR authors next, +and issue-only authors after them. Within each group, substantial features, security and correctness +fixes, proposals adopted in later work, and reports that led to fixes take priority. **Gold rings +highlight significant contributions.** The tables preserve each PR's actual status. -Upstream MinIO authorship is recorded separately: this fork derives from -[`minio/minio`](https://github.com/minio/minio), [`NOTICE`](NOTICE) retains the upstream product -notice, and the Git history carries the full upstream commit record. +Automated accounts are excluded from the community count. Upstream MinIO authorship remains in +the Git history and [NOTICE](NOTICE); this record covers activity in the PGSTY repositories. -## Code +

+@Vonng +@h5vx +@mrjavadseydi +@Dansyuqri +@ycjlin +@pinginfo +@ZouhairCharef +@mfredenhagen +@waterkip +@mikemikimike +@metaneutrons +@magicxor +@davinkevin +@lem21h +@sulin37392 +@cbornet +@vampywiz17 +@mumu-lab +@jvasile +@pmezhuev +@TLINDEN +@makinikm +@meesudzu +@kuldeep-link11 +@sargarass +@liuhaodongliu990-cmyk +@Xavier-777 +@spaceg00se-r +@kh0mka +@bagutzu +@DestroyLee +@mosesdd +@zylpsrs +@heroes1412 +@redfoxfox +@jiadzh +@AntonOfTheWoods +@chalukyaj +@nsanitate +@Kesavaambati +

-Contributors whose changes are merged into `main`. +## Merged pull requests -| Contributor | Change | Pull request | Commit | +| Contributor | Contribution | Record | +| :-- | :-- | :-- | +| [@Vonng](https://github.com/Vonng) | Maintains SILO, Console, mcli, shared packages, releases, and documentation | 85 merged PRs across five repositories; [complete maintainer record](#maintainer-record) | +| [@h5vx](https://github.com/h5vx) | Implemented per-bucket CORS configuration and enforcement | [pgsty/silo#71](https://github.com/pgsty/silo/pull/71) — Merged ([e4e3007da](https://github.com/pgsty/silo/commit/e4e3007da6d7d1198a6a050e34f84566d40a9654)) | +| [@mrjavadseydi](https://github.com/mrjavadseydi) | Fixed effective bucket quota metrics; proposed access-frequency ILM | [pgsty/silo#132](https://github.com/pgsty/silo/pull/132) — Merged ([ad873c735](https://github.com/pgsty/silo/commit/ad873c73571b121293791f13f8dc46ddf5264d60))
[pgsty/silo#60](https://github.com/pgsty/silo/pull/60) — Open | +| [@Dansyuqri](https://github.com/Dansyuqri) | Added ChecksumType to multipart completion responses | [pgsty/silo#57](https://github.com/pgsty/silo/pull/57) — Merged ([a96116b12](https://github.com/pgsty/silo/commit/a96116b128bbf2aa42f85eafbf75eb6636cd36ee)) | +| [@ycjlin](https://github.com/ycjlin) | Fixed missing-bucket ListObjects semantics | [pgsty/silo#37](https://github.com/pgsty/silo/pull/37) — Merged ([49c8aeac4](https://github.com/pgsty/silo/commit/49c8aeac403916f52f8588bbe8ee42753d86eeef)) | +| [@pinginfo](https://github.com/pinginfo) | Repaired bucket notification streaming | [pgsty/silo#34](https://github.com/pgsty/silo/pull/34) — Merged ([b7f52ca43](https://github.com/pgsty/silo/commit/b7f52ca4336bc45a48b81b39c8983a5e6882a6fa)) | +| [@ZouhairCharef](https://github.com/ZouhairCharef) | Patched CVE-2026-34986 in go-jose | [pgsty/silo#18](https://github.com/pgsty/silo/pull/18) — Merged ([ff58df949](https://github.com/pgsty/silo/commit/ff58df9499865f0ed00457d282ede219091b1f33)) | +| [@mfredenhagen](https://github.com/mfredenhagen) | Patched CVE-2026-39883 in OpenTelemetry | [pgsty/silo#19](https://github.com/pgsty/silo/pull/19) — Merged ([e4fa06394](https://github.com/pgsty/silo/commit/e4fa063942151a8b83288700f28c911376cdca9b)) | +| [@waterkip](https://github.com/waterkip) | Repointed documentation links to the SILO portal | [pgsty/silo#41](https://github.com/pgsty/silo/pull/41) — Merged ([0ce4b0f14](https://github.com/pgsty/silo/commit/0ce4b0f14bcbcbd353270ad8d9e0919c21e0ca5e)) | + +## Other pull-request authors + +Every proposal counts, including PRs closed without merging. Authors who also have a merged PR +appear in the section above, with their open proposals retained in the same record. + +| Contributor | Contribution | Pull request | Status | | :-- | :-- | :-- | :-- | -| [@ZouhairCharef](https://github.com/ZouhairCharef) | Upgraded `go-jose` to v4.1.4 to patch CVE-2026-34986 | [#18](https://github.com/pgsty/silo/pull/18) | [`68e0ba9`](https://github.com/pgsty/silo/commit/68e0ba997) | -| [@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) | +| [@mikemikimike](https://github.com/mikemikimike) | Contributed the replicated SSE-C plaintext part-size fix | [pgsty/silo#125](https://github.com/pgsty/silo/pull/125) | Closed as superseded; its core plaintext-size fix landed through [silo#124](https://github.com/pgsty/silo/pull/124), with related attribute handling in [silo#128](https://github.com/pgsty/silo/pull/128) | +| [@metaneutrons](https://github.com/metaneutrons) | Reported and proposed explicit-version delete authorization | [pgsty/silo#59](https://github.com/pgsty/silo/pull/59) | Closed as superseded; established the action split implemented in [silo#104](https://github.com/pgsty/silo/pull/104) | +| [@magicxor](https://github.com/magicxor) | Reported and proposed conditional DELETE support for If-Match | [pgsty/silo#12](https://github.com/pgsty/silo/pull/12) | Closed after conditional DELETE support landed in [silo#145](https://github.com/pgsty/silo/pull/145) | +| [@davinkevin](https://github.com/davinkevin) | Proposed the distroless container image and dependency automation | [pgsty/silo#21](https://github.com/pgsty/silo/pull/21) | Closed as superseded; the distroless image shipped in [RELEASE.2026-08-06](https://github.com/pgsty/silo/releases/tag/RELEASE.2026-08-06T00-00-00Z) | +| [@lem21h](https://github.com/lem21h) | Proposed robustness and goroutine improvements | [pgsty/silo#36](https://github.com/pgsty/silo/pull/36) | Closed without merging | +| [@sulin37392](https://github.com/sulin37392) | Proposed dependency updates | [pgsty/silo#8](https://github.com/pgsty/silo/pull/8) | Closed without merging | -## Proposed changes +## Issue reports -Pull requests that are open for review, or that were closed after informing work that shipped -differently. +Reports are retained whether open or closed. PR authors who also opened issues are credited here +for their reports as well; the avatar wall and community total still count each person once. -| 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 | - -## Reports - -Bug reports, compatibility findings, and proposals filed against this fork. Several shipped fixes -trace directly back to these: the bundled-client guarantee (#4, #9), the LDAP-over-TLS repair (#15), -the completed native package payload (#33), GPG-signed RPMs (#43), and the upstream migration guide -(#42). - -| Contributor | Reported | +| Contributor | Reports | | :-- | :-- | -| [@mosesdd](https://github.com/mosesdd) | [#1](https://github.com/pgsty/silo/issues/1) Helm chart availability | -| [@Xavier-777](https://github.com/Xavier-777) | [#2](https://github.com/pgsty/silo/issues/2) Console bucket lifecycle management · [#17](https://github.com/pgsty/silo/issues/17) Log and XML file preview | -| [@jiadzh](https://github.com/jiadzh) | [#3](https://github.com/pgsty/silo/issues/3) Windows build guidance | -| [@TLINDEN](https://github.com/TLINDEN) | [#4](https://github.com/pgsty/silo/issues/4) `mc` missing from released tarballs | -| [@AntonOfTheWoods](https://github.com/AntonOfTheWoods) | [#5](https://github.com/pgsty/silo/issues/5) Upstream Helm chart and operator options | -| [@zylpsrs](https://github.com/zylpsrs) | [#6](https://github.com/pgsty/silo/issues/6) Console missing Tiering and Site Replication | -| [@nsanitate](https://github.com/nsanitate) | [#7](https://github.com/pgsty/silo/issues/7) CNCF Sandbox governance proposal | -| [@makinikm](https://github.com/makinikm) | [#9](https://github.com/pgsty/silo/issues/9) `mc` missing from the Docker image | -| [@magicxor](https://github.com/magicxor) | [#10](https://github.com/pgsty/silo/issues/10) `DeleteObject` ignores the `If-Match` header | -| [@spaceg00se-r](https://github.com/spaceg00se-r) | [#11](https://github.com/pgsty/silo/issues/11) `cpuv1` support · [#14](https://github.com/pgsty/silo/issues/14) Project workflow token failure | -| [@heroes1412](https://github.com/heroes1412) | [#13](https://github.com/pgsty/silo/issues/13) Profile option unusable | -| [@vampywiz17](https://github.com/vampywiz17) | [#15](https://github.com/pgsty/silo/issues/15) LDAP TLS regression breaking Console login on Kubernetes | -| [@davinkevin](https://github.com/davinkevin) | [#20](https://github.com/pgsty/silo/issues/20) Renovate for automated dependency updates | -| [@chalukyaj](https://github.com/chalukyaj) | [#30](https://github.com/pgsty/silo/issues/30) Silo Operator discoverability | -| [@cbornet](https://github.com/cbornet) | [#31](https://github.com/pgsty/silo/issues/31) Multipart uploads with `FULL_OBJECT` CRC32 · [#32](https://github.com/pgsty/silo/issues/32) `ListObjects` bucket-existence semantics | -| [@jvasile](https://github.com/jvasile) | [#33](https://github.com/pgsty/silo/issues/33) `.deb` missing user, group, and default files | -| [@Kesavaambati](https://github.com/Kesavaambati) | [#35](https://github.com/pgsty/silo/issues/35) Community support for the Docker images | -| [@redfoxfox](https://github.com/redfoxfox) | [#38](https://github.com/pgsty/silo/issues/38) Chinese documentation site unreachable | -| [@kuldeep-link11](https://github.com/kuldeep-link11) | [#39](https://github.com/pgsty/silo/issues/39) `notify_nats` rejects JWT credentials files · [#40](https://github.com/pgsty/silo/issues/40) `notify_nats` target changes require a restart | -| [@meesudzu](https://github.com/meesudzu) | [#42](https://github.com/pgsty/silo/issues/42) Migration guide from upstream MinIO | -| [@pmezhuev](https://github.com/pmezhuev) | [#43](https://github.com/pgsty/silo/issues/43) RPM package missing its GPG signature | -| [@kh0mka](https://github.com/kh0mka) | [#51](https://github.com/pgsty/silo/issues/51) Inter-node I/O timeout in `ReadFileStreamHandler` | +| [@Vonng](https://github.com/Vonng) | 101 issues across SILO, Console, and mcli; [complete maintainer record](#maintainer-record) | +| [@metaneutrons](https://github.com/metaneutrons) | [pgsty/silo#58](https://github.com/pgsty/silo/issues/58) Authorize explicit object-version deletes with s3:DeleteObjectVersion | +| [@magicxor](https://github.com/magicxor) | [pgsty/silo#10](https://github.com/pgsty/silo/issues/10) DeleteObject ignores `If-Match` header (no conditional delete support) | +| [@davinkevin](https://github.com/davinkevin) | [pgsty/silo#20](https://github.com/pgsty/silo/issues/20) Proposal: Enable Renovate for automated dependency updates | +| [@cbornet](https://github.com/cbornet) | [pgsty/silo#31](https://github.com/pgsty/silo/issues/31) Multipart uploads with FULL_OBJECT CRC32 not working
[pgsty/silo#32](https://github.com/pgsty/silo/issues/32) `listObjects` should return `NoSuchBucket` when the bucket doesn't exist and prefix is passed
[pgsty/silo#107](https://github.com/pgsty/silo/issues/107) PutObject fails with chunked encoding and checksumType | +| [@vampywiz17](https://github.com/vampywiz17) | [pgsty/silo#15](https://github.com/pgsty/silo/issues/15) LDAP TLS regression in RELEASE.2026-03-21T00-00-00Z breaks built-in Console and external Console LDAP login on Kubernetes Tenant
[pgsty/silo#108](https://github.com/pgsty/silo/issues/108) Web Console login regression in RELEASE.2026-09-03T13-18-01Z (local and LDAP users fail) | +| [@mumu-lab](https://github.com/mumu-lab) | [pgsty/silo#106](https://github.com/pgsty/silo/issues/106) 监控指标读取已弃用的 BucketQuota.Quota 字段导致 Quota 指标无值 | +| [@jvasile](https://github.com/jvasile) | [pgsty/silo#33](https://github.com/pgsty/silo/issues/33) .deb doesn't create user/group/default files | +| [@pmezhuev](https://github.com/pmezhuev) | [pgsty/silo#43](https://github.com/pgsty/silo/issues/43) RPM package for RELEASE.2026-06-18T00-00-00Z is missing GPG signature | +| [@TLINDEN](https://github.com/TLINDEN) | [pgsty/silo#4](https://github.com/pgsty/silo/issues/4) mc is missing in released tarballs | +| [@makinikm](https://github.com/makinikm) | [pgsty/silo#9](https://github.com/pgsty/silo/issues/9) mc missing from pgsty/minio Docker image | +| [@meesudzu](https://github.com/meesudzu) | [pgsty/silo#42](https://github.com/pgsty/silo/issues/42) Documentation: Add migration guide from upstream MinIO to this community fork | +| [@kuldeep-link11](https://github.com/kuldeep-link11) | [pgsty/silo#39](https://github.com/pgsty/silo/issues/39) notify_nats rejects MINIO_NOTIFY_NATS_USER_CREDENTIALS (JWT creds file path)
[pgsty/silo#40](https://github.com/pgsty/silo/issues/40) notify_nats target changes require MinIO restart (no hot reload for new target IDs) | +| [@sargarass](https://github.com/sargarass) | [pgsty/silo#79](https://github.com/pgsty/silo/issues/79) ListMultipartUploads: `prefix` matches only an exact key; `max-uploads`, `key-marker` and `delimiter` are ignored | +| [@liuhaodongliu990-cmyk](https://github.com/liuhaodongliu990-cmyk) | [pgsty/silo#62](https://github.com/pgsty/silo/issues/62) fix: show indeterminate progress instead of NaN% when downloading a prefix/folder | +| [@Xavier-777](https://github.com/Xavier-777) | [pgsty/silo#2](https://github.com/pgsty/silo/issues/2) WebUI Console 的Bucket无法管理生命周期
[pgsty/silo#17](https://github.com/pgsty/silo/issues/17) Why can't log and xml files be previewed? | +| [@spaceg00se-r](https://github.com/spaceg00se-r) | [pgsty/silo#11](https://github.com/pgsty/silo/issues/11) Please support cpuv1
[pgsty/silo#14](https://github.com/pgsty/silo/issues/14) Add issue to project workflow fails due to missing github-token | +| [@kh0mka](https://github.com/kh0mka) | [pgsty/silo#51](https://github.com/pgsty/silo/issues/51) Inter-node i/o timeout in ReadFileStreamHandler during storage REST API call | +| [@bagutzu](https://github.com/bagutzu) | [pgsty/silo#61](https://github.com/pgsty/silo/issues/61) Maintain KES-compatible external KMS support, including OpenBao | +| [@DestroyLee](https://github.com/DestroyLee) | [pgsty/silo.pgsty.com#4](https://github.com/pgsty/silo.pgsty.com/issues/4) 文档目录更新没了 | +| [@mosesdd](https://github.com/mosesdd) | [pgsty/silo#1](https://github.com/pgsty/silo/issues/1) update and provide helm chart | +| [@zylpsrs](https://github.com/zylpsrs) | [pgsty/silo#6](https://github.com/pgsty/silo/issues/6) 管理控制台缺少Tiering、Site Replication | +| [@heroes1412](https://github.com/heroes1412) | [pgsty/silo#13](https://github.com/pgsty/silo/issues/13) cannot use profile | +| [@redfoxfox](https://github.com/redfoxfox) | [pgsty/silo#38](https://github.com/pgsty/silo/issues/38) Chinese Doc site availability? | +| [@jiadzh](https://github.com/jiadzh) | [pgsty/silo#3](https://github.com/pgsty/silo/issues/3) 能编译Windows版的吗?或者您能简单指导下怎么进行windows下EXE编辑么? | +| [@AntonOfTheWoods](https://github.com/AntonOfTheWoods) | [pgsty/silo#5](https://github.com/pgsty/silo/issues/5) clarify options for upstream helm chart and operator | +| [@chalukyaj](https://github.com/chalukyaj) | [pgsty/silo#30](https://github.com/pgsty/silo/issues/30) Proposal: Evaluate silo-operator and add it to README.md to allow k8s users to discover it | +| [@nsanitate](https://github.com/nsanitate) | [pgsty/silo#7](https://github.com/pgsty/silo/issues/7) Proposal: Apply for CNCF Sandbox to ensure long-term open governance and ecosystem trust | +| [@Kesavaambati](https://github.com/Kesavaambati) | [pgsty/silo#35](https://github.com/pgsty/silo/issues/35) Request for Community Support and Future Maintenance of MinIO Docker Images | -## Adding yourself +## Audit scope -Contributions are accepted inbound=outbound under AGPL-3.0-or-later with no CLA; see -[`CONTRIBUTING.md`](CONTRIBUTING.md). Merged pull requests are added here at the next release. If a -contribution is missing or recorded incorrectly, open an issue or say so on the pull request and it -will be fixed. +All issue and PR pages were read without a date cutoff. Counts below include automated accounts; +the **40-person** roll excludes the two bots, Copilot and dependabot. The maintained product stack +is SILO, Console, mcli, and silo-pkg; the SDK, KES, website, and older documentation repository were +also checked for community submissions. + +| Repository | Issues | Pull requests | +| :-- | --: | --: | +| [pgsty/silo](https://github.com/pgsty/silo) | 82 | 61 | +| [pgsty/silo-console](https://github.com/pgsty/silo-console) | 34 | 10 | +| [pgsty/mc](https://github.com/pgsty/mc) | 18 | 17 | +| [pgsty/silo-pkg](https://github.com/pgsty/silo-pkg) | 0 | 3 | +| [pgsty/silo-go](https://github.com/pgsty/silo-go) | 0 | 0 | +| [pgsty/kes](https://github.com/pgsty/kes) | 0 | 0 | +| [pgsty/silo.pgsty.com](https://github.com/pgsty/silo.pgsty.com) | 1 | 11 | +| [pgsty/minio-docs](https://github.com/pgsty/minio-docs) | 0 | 0 | +| **Total** | **135** | **102** | + +## Maintainer record + +
+@Vonng — 85 merged PRs and 101 issues + +### pgsty/silo + +**Merged PRs (46):** [pgsty/silo#44](https://github.com/pgsty/silo/pull/44 "helm: harden Silo chart delivery"), [pgsty/silo#45](https://github.com/pgsty/silo/pull/45 "helm: trim hardening to essentials"), [pgsty/silo#56](https://github.com/pgsty/silo/pull/56 "docs: adopt no-CLA + DCO policy and fix copyright terms"), [pgsty/silo#66](https://github.com/pgsty/silo/pull/66 "fix: checksum CopyObject data before compression"), [pgsty/silo#69](https://github.com/pgsty/silo/pull/69 "fix: preserve transform state on metadata-only copies"), [pgsty/silo#70](https://github.com/pgsty/silo/pull/70 "fix: return checksums from CopyObject"), [pgsty/silo#72](https://github.com/pgsty/silo/pull/72 "fix: return the remote part checksum to federated UploadPartCopy"), [pgsty/silo#73](https://github.com/pgsty/silo/pull/73 "fix: authorize user status changes by target status"), [pgsty/silo#74](https://github.com/pgsty/silo/pull/74 "fix: align multipart completion checksum errors"), [pgsty/silo#80](https://github.com/pgsty/silo/pull/80 "fix: complete per-bucket CORS release hardening"), [pgsty/silo#81](https://github.com/pgsty/silo/pull/81 "test: cover asymmetric CORS site counts"), [pgsty/silo#85](https://github.com/pgsty/silo/pull/85 "fix: restore pre-release server corrections"), [pgsty/silo#86](https://github.com/pgsty/silo/pull/86 "fix: keep rewritten CopyObject data and metadata consistent"), [pgsty/silo#87](https://github.com/pgsty/silo/pull/87 "fix: authenticate SSE-C keys on zero-byte reads"), [pgsty/silo#88](https://github.com/pgsty/silo/pull/88 "fix: skip bucket CORS lookup without an origin"), [pgsty/silo#89](https://github.com/pgsty/silo/pull/89 "fix: replicate object lock config in its own field"), [pgsty/silo#90](https://github.com/pgsty/silo/pull/90 "fix: preserve bucket configs during site adoption"), [pgsty/silo#91](https://github.com/pgsty/silo/pull/91 "fix: report site replication metadata per site"), [pgsty/silo#92](https://github.com/pgsty/silo/pull/92 "fix: reject unsupported checksum assertions"), [pgsty/silo#93](https://github.com/pgsty/silo/pull/93 "fix: reject composite CRC64NVME checksums"), [pgsty/silo#94](https://github.com/pgsty/silo/pull/94 "ci: make server release retries tag-idempotent"), [pgsty/silo#95](https://github.com/pgsty/silo/pull/95 "fix: authenticate SSE-C for object attributes"), [pgsty/silo#96](https://github.com/pgsty/silo/pull/96 "fix: reject composite CRC64NVME completion"), [pgsty/silo#97](https://github.com/pgsty/silo/pull/97 "deps: pin reviewed pre-release components"), [pgsty/silo#98](https://github.com/pgsty/silo/pull/98 "fix: authorize SSE-C attribute reads by replication permission"), [pgsty/silo#101](https://github.com/pgsty/silo/pull/101 "fix: harden CORS and replication request trust"), [pgsty/silo#103](https://github.com/pgsty/silo/pull/103 "fix: serialize whole-record bucket metadata updates"), [pgsty/silo#104](https://github.com/pgsty/silo/pull/104 "fix: authorize explicit version deletes with DeleteObjectVersion"), [pgsty/silo#121](https://github.com/pgsty/silo/pull/121 "fix: return 500 for unreadable objects instead of 206"), [pgsty/silo#122](https://github.com/pgsty/silo/pull/122 "fix: store raw SSE-C replicas verbatim on the destination"), [pgsty/silo#123](https://github.com/pgsty/silo/pull/123 "fix: honor a requested checksum algorithm on SSE-C key rotation"), [pgsty/silo#124](https://github.com/pgsty/silo/pull/124 "fix: record plaintext part sizes for replicated SSE-C multipart parts"), [pgsty/silo#126](https://github.com/pgsty/silo/pull/126 "fix: exclude SSE-C objects from compression"), [pgsty/silo#127](https://github.com/pgsty/silo/pull/127 "fix: include per-bucket CORS in bucket metadata export and import"), [pgsty/silo#128](https://github.com/pgsty/silo/pull/128 "fix: report logical part sizes and end pagination correctly in GetObjectAttributes"), [pgsty/silo#129](https://github.com/pgsty/silo/pull/129 "fix: order value-less replicated Object Lock updates by timestamp"), [pgsty/silo#130](https://github.com/pgsty/silo/pull/130 "docs: describe the startup readiness window of the health probes"), [pgsty/silo#131](https://github.com/pgsty/silo/pull/131 "fix: stop re-replicating an object whose retention was removed"), [pgsty/silo#134](https://github.com/pgsty/silo/pull/134 "fix: retransmit and re-order Object Lock for SSE-C replicas (single erasure set)"), [pgsty/silo#135](https://github.com/pgsty/silo/pull/135 "test: reconcile encrypted-parts attributes test with #119 write validation"), [pgsty/silo#138](https://github.com/pgsty/silo/pull/138 "fix: persist an honest resync terminal status about object counts"), [pgsty/silo#140](https://github.com/pgsty/silo/pull/140 "fix: count resync success by replication outcome, not target existence"), [pgsty/silo#142](https://github.com/pgsty/silo/pull/142 "fix: scope resync worker dispatch to the target being resynced"), [pgsty/silo#143](https://github.com/pgsty/silo/pull/143 "fix: honor a header-delivered checksum advertised as a chunked trailer"), [pgsty/silo#145](https://github.com/pgsty/silo/pull/145 "fix: support conditional DeleteObject (If-Match) with atomic precondition"), [pgsty/silo#146](https://github.com/pgsty/silo/pull/146 "fix: keep embedded Console login working over loopback TLS (#108)"). + +**Issues (49):** [pgsty/silo#22](https://github.com/pgsty/silo/issues/22 "[Security] CVE-2026-33322: JWT Algorithm Confusion in OIDC Authentication"), [pgsty/silo#23](https://github.com/pgsty/silo/issues/23 "[Security] CVE-2026-33419: LDAP Login Brute-Force via User Enumeration and Missing Rate Limit"), [pgsty/silo#24](https://github.com/pgsty/silo/issues/24 "[Security] CVE-2026-34204: SSE Metadata Injection via Replication Headers (Targeted DoS)"), [pgsty/silo#25](https://github.com/pgsty/silo/issues/25 "[Security] CVE-2026-39414: Denial of Service via Unbounded Memory Allocation in S3 Select CSV Parsing"), [pgsty/silo#26](https://github.com/pgsty/silo/issues/26 "[Security] CVE-2026-32285: Potential vulnerability in third-party dependency github.com/buger/jsonparser v1.1.2"), [pgsty/silo#27](https://github.com/pgsty/silo/issues/27 "[Security] GHSA-hv4r-mvr4-25vw: Unauthenticated Object Write via Query-String Credential Signature Bypass in Unsigned-Trailer Uploads"), [pgsty/silo#28](https://github.com/pgsty/silo/issues/28 "[Security] GHSA-9c4q-hq6p-c237: Unauthenticated Object Write via Missing Signature Verification in Snowball Auto-Extract"), [pgsty/silo#46](https://github.com/pgsty/silo/issues/46 "UploadPart requires a per-part checksum header that AWS S3 computes server-side"), [pgsty/silo#47](https://github.com/pgsty/silo/issues/47 "CompleteMultipartUpload response omits ChecksumType"), [pgsty/silo#48](https://github.com/pgsty/silo/issues/48 "CompleteMultipartUpload checksum failures return non-AWS error codes"), [pgsty/silo#49](https://github.com/pgsty/silo/issues/49 "CompleteMultipartUpload accepts duplicate part numbers and assembles the part twice"), [pgsty/silo#50](https://github.com/pgsty/silo/issues/50 "CRC64NVME + COMPOSITE is silently canonicalised to FULL_OBJECT instead of rejected"), [pgsty/silo#52](https://github.com/pgsty/silo/issues/52 "ReadParts panics on an empty part list, stranding a keep-alive goroutine per storage-REST request"), [pgsty/silo#53](https://github.com/pgsty/silo/issues/53 "Migrating a Postgres or MySQL notify config disables all bucket notifications"), [pgsty/silo#55](https://github.com/pgsty/silo/issues/55 "Release image declares VOLUME ['/data'] but never creates it, so every non-root run fails storage init"), [pgsty/silo#63](https://github.com/pgsty/silo/issues/63 "CopyObject server-side checksum can cover transformed data when compression is enabled"), [pgsty/silo#64](https://github.com/pgsty/silo/issues/64 "Federated UploadPartCopy cannot reliably return the remote computed part checksum"), [pgsty/silo#65](https://github.com/pgsty/silo/issues/65 "MINIO_CONFIG_ENV_FILE silently ignores assignments with spaces around ="), [pgsty/silo#67](https://github.com/pgsty/silo/issues/67 "Metadata-only CopyObject can stamp compression metadata without rewriting data"), [pgsty/silo#68](https://github.com/pgsty/silo/issues/68 "CopyObjectResult omits checksum fields and minio-go drops them"), [pgsty/silo#75](https://github.com/pgsty/silo/issues/75 "Harden per-bucket CORS replication, recovery, and compatibility after #71"), [pgsty/silo#76](https://github.com/pgsty/silo/issues/76 "Fix Object Lock metadata field in site-replication initial sync"), [pgsty/silo#77](https://github.com/pgsty/silo/issues/77 "Audit site-replication source timestamps, tombstones, and per-site counters"), [pgsty/silo#78](https://github.com/pgsty/silo/issues/78 "Preserve existing Object Lock retention during site-replication bucket adoption"), [pgsty/silo#82](https://github.com/pgsty/silo/issues/82 "Zero-byte SSE-C objects never authenticate the customer key"), [pgsty/silo#83](https://github.com/pgsty/silo/issues/83 "Metadata-only CopyObject of a null version rewrites the data and leaves the metadata inconsistent"), [pgsty/silo#84](https://github.com/pgsty/silo/issues/84 "GetObjectAttributes does not authenticate the SSE-C key"), [pgsty/silo#99](https://github.com/pgsty/silo/issues/99 "Federated CopyObject silently ignores a requested checksum algorithm"), [pgsty/silo#100](https://github.com/pgsty/silo/issues/100 "Federated CopyObject rejects any source object stored inline"), [pgsty/silo#102](https://github.com/pgsty/silo/issues/102 "Serialize whole-record bucket metadata updates across config types"), [pgsty/silo#105](https://github.com/pgsty/silo/issues/105 "Audit residual bucket-metadata delete/update and cache ordering"), [pgsty/silo#109](https://github.com/pgsty/silo/issues/109 "[P1] SSE-C replicas become unreadable with destination default encryption or compression"), [pgsty/silo#110](https://github.com/pgsty/silo/issues/110 "[P1] Unreadable-object errors return HTTP 206 and are accepted as successful reads"), [pgsty/silo#111](https://github.com/pgsty/silo/issues/111 "[P1] Empty Object Lock replication updates bypass ordering and erase newer object state"), [pgsty/silo#112](https://github.com/pgsty/silo/issues/112 "[P2] Bucket metadata export/import silently loses per-bucket CORS configuration"), [pgsty/silo#113](https://github.com/pgsty/silo/issues/113 "[P2] In-place SSE-C key rotation ignores a requested checksum algorithm"), [pgsty/silo#114](https://github.com/pgsty/silo/issues/114 "[P2] GetObjectAttributes reports physical sizes for compressed or encrypted multipart parts"), [pgsty/silo#115](https://github.com/pgsty/silo/issues/115 "[P2] GetObjectAttributes pagination cannot finish with sparse multipart part numbers"), [pgsty/silo#116](https://github.com/pgsty/silo/issues/116 "[P2] Four-node restart reports online before every coordinator can read and write"), [pgsty/silo#117](https://github.com/pgsty/silo/issues/117 "[P2] Retention-removal replication mishandles empty-value and timestamp-only state"), [pgsty/silo#118](https://github.com/pgsty/silo/issues/118 "[P2] A compressed SSE-C object replicates to a replica that decrypts to an S2 stream"), [pgsty/silo#119](https://github.com/pgsty/silo/issues/119 "[P1] Replicated SSE-C multipart parts record the ciphertext length, corrupting part-number reads and later the object size"), [pgsty/silo#120](https://github.com/pgsty/silo/issues/120 "[P1] Existing SSE-C replicas cannot always be updated or repaired"), [pgsty/silo#133](https://github.com/pgsty/silo/issues/133 "[P2] Multi-pool: replica-write Object Lock reconciliation is not authoritative across server pools"), [pgsty/silo#136](https://github.com/pgsty/silo/issues/136 "[P2] Resync publishes Completed before the final object counters are persisted"), [pgsty/silo#137](https://github.com/pgsty/silo/issues/137 "resync cancellation robustness: dispatch deadlock, walker leak, and per-bucket cancel routing"), [pgsty/silo#139](https://github.com/pgsty/silo/issues/139 "[P2] Resync counts an existing replica as successful even when its update failed"), [pgsty/silo#141](https://github.com/pgsty/silo/issues/141 "resync worker dispatch is any-target, not scoped to the resync ARN"), [pgsty/silo#144](https://github.com/pgsty/silo/issues/144 "conditional DeleteObject (If-Match) is atomic only within a single erasure set"). + +### pgsty/silo-console + +**Merged PRs (10):** [pgsty/silo-console#9](https://github.com/pgsty/silo-console/pull/9 "deps: pin the pre-release MCLI source"), [pgsty/silo-console#10](https://github.com/pgsty/silo-console/pull/10 "deps: pin the remote env scheme repair"), [pgsty/silo-console#11](https://github.com/pgsty/silo-console/pull/11 "ci: test the upstream package floor on a coherent module graph"), [pgsty/silo-console#38](https://github.com/pgsty/silo-console/pull/38 "Prepare the next release and migrate to silo-pkg v3.13.0"), [pgsty/silo-console#39](https://github.com/pgsty/silo-console/pull/39 "Stabilize TestCafe role session capture"), [pgsty/silo-console#40](https://github.com/pgsty/silo-console/pull/40 "Finalize the SILO Console v2.3.0 candidate"), [pgsty/silo-console#41](https://github.com/pgsty/silo-console/pull/41 "test: stabilize rewind route coverage"), [pgsty/silo-console#42](https://github.com/pgsty/silo-console/pull/42 "release: simplify the v2.3.0 publish path"), [pgsty/silo-console#43](https://github.com/pgsty/silo-console/pull/43 "build: prepare SILO Console v2.3.1"), [pgsty/silo-console#44](https://github.com/pgsty/silo-console/pull/44 "fix: prioritize the maintained SILO dependency graph"). + +**Issues (34):** [pgsty/silo-console#1](https://github.com/pgsty/silo-console/issues/1 "Unauthenticated deep routes recurse `/login` indefinitely; plain HTTP triggers reliably"), [pgsty/silo-console#2](https://github.com/pgsty/silo-console/issues/2 "Metrics and Object Browser UI regressions: stale Uptime, malformed legends, and cramped menus"), [pgsty/silo-console#3](https://github.com/pgsty/silo-console/issues/3 "390 px viewport clips Metrics tabs and Object Browser content with no horizontal recovery"), [pgsty/silo-console#4](https://github.com/pgsty/silo-console/issues/4 "Collapsed and mobile sidebar exposes unnamed icon-only buttons"), [pgsty/silo-console#5](https://github.com/pgsty/silo-console/issues/5 "Access Key credential fields lack autocomplete metadata and trigger Chrome warnings"), [pgsty/silo-console#6](https://github.com/pgsty/silo-console/issues/6 "Add English/Chinese localization with an in-place language toggle"), [pgsty/silo-console#7](https://github.com/pgsty/silo-console/issues/7 "Migrate Console monitoring queries to MinIO Metrics V3"), [pgsty/silo-console#8](https://github.com/pgsty/silo-console/issues/8 "Replace N/A Info metrics with actionable V3 health signals"), [pgsty/silo-console#12](https://github.com/pgsty/silo-console/issues/12 "Trust forwarding headers only from explicitly configured proxies"), [pgsty/silo-console#13](https://github.com/pgsty/silo-console/issues/13 "Restore TLS certificate and hostname verification for outbound clients"), [pgsty/silo-console#14](https://github.com/pgsty/silo-console/issues/14 "Redact session credentials from detailed debug logs"), [pgsty/silo-console#15](https://github.com/pgsty/silo-console/issues/15 "Bound and validate Object Manager WebSocket sessions"), [pgsty/silo-console#16](https://github.com/pgsty/silo-console/issues/16 "Prevent stale object requests from acting on the wrong object"), [pgsty/silo-console#17](https://github.com/pgsty/silo-console/issues/17 "Close the maintained dependency release chain before the next Console tag"), [pgsty/silo-console#18](https://github.com/pgsty/silo-console/issues/18 "Keep the documented downstream replacement set synchronized with go.mod"), [pgsty/silo-console#19](https://github.com/pgsty/silo-console/issues/19 "Require the complete validation matrix for tagged releases"), [pgsty/silo-console#20](https://github.com/pgsty/silo-console/issues/20 "Prepare authoritative version metadata and embedded assets for the next release"), [pgsty/silo-console#21](https://github.com/pgsty/silo-console/issues/21 "Include license, notice, and attribution material in every release artifact"), [pgsty/silo-console#22](https://github.com/pgsty/silo-console/issues/22 "Release upload references after aborts and network failures"), [pgsty/silo-console#23](https://github.com/pgsty/silo-console/issues/23 "Close diagnostic WebSockets on unmount and bound client-side log history"), [pgsty/silo-console#24](https://github.com/pgsty/silo-console/issues/24 "Replace regex-based IAM resource matching with safe wildcard semantics"), [pgsty/silo-console#25](https://github.com/pgsty/silo-console/issues/25 "Unify SSO session identity and expiry handling across API clients"), [pgsty/silo-console#26](https://github.com/pgsty/silo-console/issues/26 "Stream and cancel multi-object ZIP downloads instead of buffering the full archive"), [pgsty/silo-console#27](https://github.com/pgsty/silo-console/issues/27 "Gate Create Bucket UI on s3:CreateBucket permission"), [pgsty/silo-console#28](https://github.com/pgsty/silo-console/issues/28 "Give every icon-only control an accessible name and fix the localized sign-out name"), [pgsty/silo-console#29](https://github.com/pgsty/silo-console/issues/29 "Handle malformed routes and persisted UI state without blank-screen crashes"), [pgsty/silo-console#30](https://github.com/pgsty/silo-console/issues/30 "Finish localization of remaining raw UI strings"), [pgsty/silo-console#31](https://github.com/pgsty/silo-console/issues/31 "Make release artifacts reproducible for a fixed commit"), [pgsty/silo-console#32](https://github.com/pgsty/silo-console/issues/32 "Add signed checksums, SBOMs, and provenance to release artifacts"), [pgsty/silo-console#33](https://github.com/pgsty/silo-console/issues/33 "Pin CI actions, tools, and browser-test dependencies immutably"), [pgsty/silo-console#34](https://github.com/pgsty/silo-console/issues/34 "Align source-container builds and image publication with the release contract"), [pgsty/silo-console#35](https://github.com/pgsty/silo-console/issues/35 "Harden the systemd service and document certificate-directory ownership"), [pgsty/silo-console#36](https://github.com/pgsty/silo-console/issues/36 "Encode Watch WebSocket filters with URLSearchParams"), [pgsty/silo-console#37](https://github.com/pgsty/silo-console/issues/37 "Preserve structured Inspect errors and avoid reading response bodies twice"). + +### pgsty/mc + +**Merged PRs (16):** [pgsty/mc#1](https://github.com/pgsty/mc/pull/1 "build: harden release provenance and package metadata"), [pgsty/mc#2](https://github.com/pgsty/mc/pull/2 "rebrand: adopt Silo client identity, close SUBNET paths, add brand gate"), [pgsty/mc#3](https://github.com/pgsty/mc/pull/3 "fix: remove vendor encryption key, close proxy-set path, tighten DCO gate"), [pgsty/mc#4](https://github.com/pgsty/mc/pull/4 "fix: repair a link and help text damaged by the brand sweep"), [pgsty/mc#8](https://github.com/pgsty/mc/pull/8 "feat: add read-only checksum verification"), [pgsty/mc#9](https://github.com/pgsty/mc/pull/9 "fix: validate policy writes strictly"), [pgsty/mc#10](https://github.com/pgsty/mc/pull/10 "fix: use regular PUT for empty pipe input"), [pgsty/mc#11](https://github.com/pgsty/mc/pull/11 "ci: make release retries tag-idempotent"), [pgsty/mc#13](https://github.com/pgsty/mc/pull/13 "release: silo-pkg v3.13.0 module path, fail-closed credential redaction, release gates"), [pgsty/mc#22](https://github.com/pgsty/mc/pull/22 "fix: close the findings of the final pre-release review"), [pgsty/mc#24](https://github.com/pgsty/mc/pull/24 "fix: avoid double-closing S3 Select responses"), [pgsty/mc#27](https://github.com/pgsty/mc/pull/27 "build: prepare the 20260903 dependency release"), [pgsty/mc#32](https://github.com/pgsty/mc/pull/32 "fix: read the applied globals for pipe's quiet and json flags"), [pgsty/mc#33](https://github.com/pgsty/mc/pull/33 "fix: accept on/off and enabled/disabled in MC_* boolean env vars"), [pgsty/mc#34](https://github.com/pgsty/mc/pull/34 "fix: keep an explicit checksum on zero-byte uploads"), [pgsty/mc#35](https://github.com/pgsty/mc/pull/35 "fix: sql exits non-zero when a query fails"). + +**Issues (18):** [pgsty/mc#5](https://github.com/pgsty/mc/issues/5 "[release gate] checksum verify drops stdout when non-TTY"), [pgsty/mc#6](https://github.com/pgsty/mc/issues/6 "[release gate] make Release workflow tag-idempotent and clean orphan drafts"), [pgsty/mc#7](https://github.com/pgsty/mc/issues/7 "[release] integrate and ship the checksum-audit mcli candidate"), [pgsty/mc#12](https://github.com/pgsty/mc/issues/12 "go.mod advertises a minio/pkg v3.6.1 floor the source cannot honor"), [pgsty/mc#14](https://github.com/pgsty/mc/issues/14 "[security][release blocker] make HTTP credential redaction fail-closed"), [pgsty/mc#15](https://github.com/pgsty/mc/issues/15 "[security][release blocker] sanitize error and JSON output before serialization"), [pgsty/mc#16](https://github.com/pgsty/mc/issues/16 "[security][release blocker] register every credential ingress before errors or network calls"), [pgsty/mc#17](https://github.com/pgsty/mc/issues/17 "[security] redact credentials from admin trace and scanner trace"), [pgsty/mc#18](https://github.com/pgsty/mc/issues/18 "[supply chain][release blocker] bind Docker artifacts to exact attestation subjects and architectures"), [pgsty/mc#19](https://github.com/pgsty/mc/issues/19 "[release gate] make artifact release tag-only and require exact-main Test Release evidence"), [pgsty/mc#20](https://github.com/pgsty/mc/issues/20 "[governance][release blocker] define review mode, split tag protection, enable immutable releases"), [pgsty/mc#21](https://github.com/pgsty/mc/issues/21 "[quality debt] archive reproducibility, test portability, and deferred compatibility gaps"), [pgsty/mc#23](https://github.com/pgsty/mc/issues/23 "sql: intermittent panic in the zstd response reader when SelectResults is closed"), [pgsty/mc#25](https://github.com/pgsty/mc/issues/25 "sql: a query error is reported but the process exits 0"), [pgsty/mc#28](https://github.com/pgsty/mc/issues/28 "[P2] Empty pipe input silently drops an explicitly requested checksum"), [pgsty/mc#29](https://github.com/pgsty/mc/issues/29 "[P2] Global --json pipe emits a progress prefix and invalid JSON"), [pgsty/mc#30](https://github.com/pgsty/mc/issues/30 "[P3] mcli pipe writes progress-bar frames into a redirected stdout, unlike every other transfer command"), [pgsty/mc#31](https://github.com/pgsty/mc/issues/31 "[P3] Every mcli command aborts when an MC_* boolean environment variable is set to on, off, enabled or disabled"). + +### pgsty/silo-pkg + +**Merged PRs (3):** [pgsty/silo-pkg#1](https://github.com/pgsty/silo-pkg/pull/1 "fix: require an exact remote env URL scheme"), [pgsty/silo-pkg#2](https://github.com/pgsty/silo-pkg/pull/2 "feat!: own the module path and drop the Silo Go SDK replacement"), [pgsty/silo-pkg#3](https://github.com/pgsty/silo-pkg/pull/3 "ci: key the concurrency group on the pull request or branch"). + +### pgsty/silo.pgsty.com + +**Merged PRs (10):** [pgsty/silo.pgsty.com#2](https://github.com/pgsty/silo.pgsty.com/pull/2 "docs: record CopyObject checksum invariant"), [pgsty/silo.pgsty.com#3](https://github.com/pgsty/silo.pgsty.com/pull/3 "docs: expand CopyObject checksum design"), [pgsty/silo.pgsty.com#5](https://github.com/pgsty/silo.pgsty.com/pull/5 "docs: record ListObjects NoSuchBucket decision"), [pgsty/silo.pgsty.com#6](https://github.com/pgsty/silo.pgsty.com/pull/6 "docs: record multipart checksum error contract"), [pgsty/silo.pgsty.com#7](https://github.com/pgsty/silo.pgsty.com/pull/7 "docs: record final bucket CORS hardening design"), [pgsty/silo.pgsty.com#8](https://github.com/pgsty/silo.pgsty.com/pull/8 "docs: finalize bucket CORS closure status"), [pgsty/silo.pgsty.com#9](https://github.com/pgsty/silo.pgsty.com/pull/9 "docs: record pre-release hardening and checksum contracts"), [pgsty/silo.pgsty.com#10](https://github.com/pgsty/silo.pgsty.com/pull/10 "docs: clarify site status accounting semantics"), [pgsty/silo.pgsty.com#11](https://github.com/pgsty/silo.pgsty.com/pull/11 "docs: record CORS trust and explicit version delete authorization"), [pgsty/silo.pgsty.com#13](https://github.com/pgsty/silo.pgsty.com/pull/13 "docs: startup readiness window on the Healthcheck API page"). + +
+ +## Keeping this record current + +New issue and PR authors are included when this record is refreshed, regardless of merge status. +Keep this file, the English and Chinese README avatar walls, and the +[website contributor data](https://github.com/pgsty/silo.pgsty.com/blob/main/data/home/contributors.yaml) +in sync. If a contribution is missing or described incorrectly, open an issue or pull request. + +Code contributions follow the no-CLA, DCO policy in [CONTRIBUTING.md](CONTRIBUTING.md). diff --git a/CREDITS b/CREDITS index 256a8bf37..819305621 100644 --- a/CREDITS +++ b/CREDITS @@ -3396,7 +3396,7 @@ SOFTWARE. github.com/cheggaaa/pb https://github.com/cheggaaa/pb ---------------------------------------------------------------- -Copyright (c) 2012-2015, Sergey Cherepanov +Copyright (c) 2012-2024, Sergey Cherepanov All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: @@ -6218,203 +6218,6 @@ SOFTWARE. ================================================================ -github.com/go-ini/ini -https://github.com/go-ini/ini ----------------------------------------------------------------- -Apache License -Version 2.0, January 2004 -http://www.apache.org/licenses/ - -TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - -1. Definitions. - -"License" shall mean the terms and conditions for use, reproduction, and -distribution as defined by Sections 1 through 9 of this document. - -"Licensor" shall mean the copyright owner or entity authorized by the copyright -owner that is granting the License. - -"Legal Entity" shall mean the union of the acting entity and all other entities -that control, are controlled by, or are under common control with that entity. -For the purposes of this definition, "control" means (i) the power, direct or -indirect, to cause the direction or management of such entity, whether by -contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the -outstanding shares, or (iii) beneficial ownership of such entity. - -"You" (or "Your") shall mean an individual or Legal Entity exercising -permissions granted by this License. - -"Source" form shall mean the preferred form for making modifications, including -but not limited to software source code, documentation source, and configuration -files. - -"Object" form shall mean any form resulting from mechanical transformation or -translation of a Source form, including but not limited to compiled object code, -generated documentation, and conversions to other media types. - -"Work" shall mean the work of authorship, whether in Source or Object form, made -available under the License, as indicated by a copyright notice that is included -in or attached to the work (an example is provided in the Appendix below). - -"Derivative Works" shall mean any work, whether in Source or Object form, that -is based on (or derived from) the Work and for which the editorial revisions, -annotations, elaborations, or other modifications represent, as a whole, an -original work of authorship. For the purposes of this License, Derivative Works -shall not include works that remain separable from, or merely link (or bind by -name) to the interfaces of, the Work and Derivative Works thereof. - -"Contribution" shall mean any work of authorship, including the original version -of the Work and any modifications or additions to that Work or Derivative Works -thereof, that is intentionally submitted to Licensor for inclusion in the Work -by the copyright owner or by an individual or Legal Entity authorized to submit -on behalf of the copyright owner. For the purposes of this definition, -"submitted" means any form of electronic, verbal, or written communication sent -to the Licensor or its representatives, including but not limited to -communication on electronic mailing lists, source code control systems, and -issue tracking systems that are managed by, or on behalf of, the Licensor for -the purpose of discussing and improving the Work, but excluding communication -that is conspicuously marked or otherwise designated in writing by the copyright -owner as "Not a Contribution." - -"Contributor" shall mean Licensor and any individual or Legal Entity on behalf -of whom a Contribution has been received by Licensor and subsequently -incorporated within the Work. - -2. Grant of Copyright License. - -Subject to the terms and conditions of this License, each Contributor hereby -grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, -irrevocable copyright license to reproduce, prepare Derivative Works of, -publicly display, publicly perform, sublicense, and distribute the Work and such -Derivative Works in Source or Object form. - -3. Grant of Patent License. - -Subject to the terms and conditions of this License, each Contributor hereby -grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, -irrevocable (except as stated in this section) patent license to make, have -made, use, offer to sell, sell, import, and otherwise transfer the Work, where -such license applies only to those patent claims licensable by such Contributor -that are necessarily infringed by their Contribution(s) alone or by combination -of their Contribution(s) with the Work to which such Contribution(s) was -submitted. If You institute patent litigation against any entity (including a -cross-claim or counterclaim in a lawsuit) alleging that the Work or a -Contribution incorporated within the Work constitutes direct or contributory -patent infringement, then any patent licenses granted to You under this License -for that Work shall terminate as of the date such litigation is filed. - -4. Redistribution. - -You may reproduce and distribute copies of the Work or Derivative Works thereof -in any medium, with or without modifications, and in Source or Object form, -provided that You meet the following conditions: - -You must give any other recipients of the Work or Derivative Works a copy of -this License; and -You must cause any modified files to carry prominent notices stating that You -changed the files; and -You must retain, in the Source form of any Derivative Works that You distribute, -all copyright, patent, trademark, and attribution notices from the Source form -of the Work, excluding those notices that do not pertain to any part of the -Derivative Works; and -If the Work includes a "NOTICE" text file as part of its distribution, then any -Derivative Works that You distribute must include a readable copy of the -attribution notices contained within such NOTICE file, excluding those notices -that do not pertain to any part of the Derivative Works, in at least one of the -following places: within a NOTICE text file distributed as part of the -Derivative Works; within the Source form or documentation, if provided along -with the Derivative Works; or, within a display generated by the Derivative -Works, if and wherever such third-party notices normally appear. The contents of -the NOTICE file are for informational purposes only and do not modify the -License. You may add Your own attribution notices within Derivative Works that -You distribute, alongside or as an addendum to the NOTICE text from the Work, -provided that such additional attribution notices cannot be construed as -modifying the License. -You may add Your own copyright statement to Your modifications and may provide -additional or different license terms and conditions for use, reproduction, or -distribution of Your modifications, or for any such Derivative Works as a whole, -provided Your use, reproduction, and distribution of the Work otherwise complies -with the conditions stated in this License. - -5. Submission of Contributions. - -Unless You explicitly state otherwise, any Contribution intentionally submitted -for inclusion in the Work by You to the Licensor shall be under the terms and -conditions of this License, without any additional terms or conditions. -Notwithstanding the above, nothing herein shall supersede or modify the terms of -any separate license agreement you may have executed with Licensor regarding -such Contributions. - -6. Trademarks. - -This License does not grant permission to use the trade names, trademarks, -service marks, or product names of the Licensor, except as required for -reasonable and customary use in describing the origin of the Work and -reproducing the content of the NOTICE file. - -7. Disclaimer of Warranty. - -Unless required by applicable law or agreed to in writing, Licensor provides the -Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, -including, without limitation, any warranties or conditions of TITLE, -NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are -solely responsible for determining the appropriateness of using or -redistributing the Work and assume any risks associated with Your exercise of -permissions under this License. - -8. Limitation of Liability. - -In no event and under no legal theory, whether in tort (including negligence), -contract, or otherwise, unless required by applicable law (such as deliberate -and grossly negligent acts) or agreed to in writing, shall any Contributor be -liable to You for damages, including any direct, indirect, special, incidental, -or consequential damages of any character arising as a result of this License or -out of the use or inability to use the Work (including but not limited to -damages for loss of goodwill, work stoppage, computer failure or malfunction, or -any and all other commercial damages or losses), even if such Contributor has -been advised of the possibility of such damages. - -9. Accepting Warranty or Additional Liability. - -While redistributing the Work or Derivative Works thereof, You may choose to -offer, and charge a fee for, acceptance of support, warranty, indemnity, or -other liability obligations and/or rights consistent with this License. However, -in accepting such obligations, You may act only on Your own behalf and on Your -sole responsibility, not on behalf of any other Contributor, and only if You -agree to indemnify, defend, and hold each Contributor harmless for any liability -incurred by, or claims asserted against, such Contributor by reason of your -accepting any such warranty or additional liability. - -END OF TERMS AND CONDITIONS - -APPENDIX: How to apply the Apache License to your work - -To apply the Apache License to your work, attach the following boilerplate -notice, with the fields enclosed by brackets "[]" replaced with your own -identifying information. (Don't include the brackets!) The text should be -enclosed in the appropriate comment syntax for the file format. We also -recommend that a file or class name and description of purpose be included on -the same "printed page" as the copyright notice for easier identification within -third-party archives. - - Copyright 2014 Unknwon - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - -================================================================ - github.com/go-jose/go-jose/v4 https://github.com/go-jose/go-jose/v4 ---------------------------------------------------------------- @@ -8444,6 +8247,214 @@ THE SOFTWARE. ================================================================ +github.com/go-openapi/runtime/server-middleware +https://github.com/go-openapi/runtime/server-middleware +---------------------------------------------------------------- + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +================================================================ + github.com/go-openapi/spec https://github.com/go-openapi/spec ---------------------------------------------------------------- @@ -9692,214 +9703,6 @@ https://github.com/go-openapi/swag/fileutils ================================================================ -github.com/go-openapi/swag/jsonname -https://github.com/go-openapi/swag/jsonname ----------------------------------------------------------------- - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - -================================================================ - github.com/go-openapi/swag/jsonutils https://github.com/go-openapi/swag/jsonutils ---------------------------------------------------------------- @@ -10732,6 +10535,214 @@ https://github.com/go-openapi/swag/netutils ================================================================ +github.com/go-openapi/swag/pools +https://github.com/go-openapi/swag/pools +---------------------------------------------------------------- + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +================================================================ + github.com/go-openapi/swag/stringutils https://github.com/go-openapi/swag/stringutils ---------------------------------------------------------------- @@ -16641,33 +16652,6 @@ SOFTWARE. ================================================================ -github.com/lestrrat-go/option -https://github.com/lestrrat-go/option ----------------------------------------------------------------- -MIT License - -Copyright (c) 2021 lestrrat-go - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - -================================================================ - github.com/lestrrat-go/option/v2 https://github.com/lestrrat-go/option/v2 ---------------------------------------------------------------- @@ -17823,20 +17807,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 . + - 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 +. + +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). ================================================================ @@ -21713,6 +21744,14 @@ separate copyright notices and license terms. Your use of the source code for the these subcomponents is subject to the terms and conditions of the following licenses. +=== + +This distribution (pgsty/mc, shipped as "mcli") is a community-maintained +fork of the MinIO Client, modified by the Silo project +(https://silo.pgsty.com/). It is not affiliated with, endorsed by, or +sponsored by MinIO, Inc. Modifications are Copyright (c) 2025-2026 PGSTY +and are licensed under the GNU AGPL v3.0 or later. + ================================================================ github.com/minio/md5-simd @@ -22176,8 +22215,8 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ================================================================ -github.com/minio/pkg/v3 (replaced by github.com/pgsty/silo-pkg/v3) -https://github.com/pgsty/silo-pkg/v3 +github.com/minio/pkg/v3 +https://github.com/minio/pkg/v3 ---------------------------------------------------------------- GNU AFFERO GENERAL PUBLIC LICENSE Version 3, 19 November 2007 @@ -25447,6 +25486,673 @@ THE SOFTWARE. ================================================================ +github.com/pgsty/silo-pkg/v3 +https://github.com/pgsty/silo-pkg/v3 +---------------------------------------------------------------- + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. + +================================================================ + github.com/philhofer/fwd https://github.com/philhofer/fwd ---------------------------------------------------------------- @@ -27032,8 +27738,14 @@ See https://github.com/mantinedev/mantine/blob/master/LICENSE for license detail We also use code from a large number of npm packages. For details, see: - https://github.com/prometheus/prometheus/blob/main/web/ui/react-app/package.json - https://github.com/prometheus/prometheus/blob/main/web/ui/react-app/package-lock.json -- The individual package licenses as copied from the node_modules directory can be found in - the npm_licenses.tar.bz2 archive in release tarballs and Docker images. +- https://github.com/prometheus/prometheus/blob/main/web/ui/mantine-ui/package.json +- https://github.com/prometheus/prometheus/blob/main/web/ui/pnpm-lock.yaml +- The individual licenses of the packages bundled into the new (mantine) web UI + are collected at build time, embedded in the Prometheus binary, and served by + the web UI at /assets/third-party-licenses.txt. +- The licenses of the packages bundled into the old (react-app) web UI, served + via --enable-feature=old-ui, are extracted at build time into a + *.LICENSE.txt file that is embedded alongside the old UI's JavaScript bundle. ================================================================ @@ -28337,6 +29049,36 @@ SOFTWARE. ================================================================ +github.com/vbauerster/cupwriter +https://github.com/vbauerster/cupwriter +---------------------------------------------------------------- +This is free and unencumbered software released into the public domain. + +Anyone is free to copy, modify, publish, use, compile, sell, or +distribute this software, either in source code form or as a compiled +binary, for any purpose, commercial or non-commercial, and by any +means. + +In jurisdictions that recognize copyright laws, the author or authors +of this software dedicate any and all copyright interest in the +software to the public domain. We make this dedication for the benefit +of the public at large and to the detriment of our heirs and +successors. We intend this dedication to be an overt act of +relinquishment in perpetuity of all present and future rights to this +software under copyright law. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR +OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + +For more information, please refer to + +================================================================ + github.com/vbauerster/mpb/v8 https://github.com/vbauerster/mpb/v8 ---------------------------------------------------------------- @@ -28982,7 +29724,7 @@ https://go.etcd.io/etcd/api/v3 same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright [yyyy] [name of copyright owner] + Copyright 2020 The etcd Authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -29190,7 +29932,7 @@ https://go.etcd.io/etcd/client/pkg/v3 same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright [yyyy] [name of copyright owner] + Copyright 2020 The etcd Authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -29398,7 +30140,7 @@ https://go.etcd.io/etcd/client/v3 same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright [yyyy] [name of copyright owner] + Copyright 2020 The etcd Authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -31592,229 +32334,6 @@ THE SOFTWARE. ================================================================ -go.yaml.in/yaml/v2 -https://go.yaml.in/yaml/v2 ----------------------------------------------------------------- - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "{}" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright {yyyy} {name of copyright owner} - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - -Bundled NOTICE file: - -Copyright 2011-2016 Canonical Ltd. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. - -================================================================ - go.yaml.in/yaml/v3 https://go.yaml.in/yaml/v3 ---------------------------------------------------------------- @@ -33091,6 +33610,203 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ================================================================ +gopkg.in/ini.v1 +https://gopkg.in/ini.v1 +---------------------------------------------------------------- +Apache License +Version 2.0, January 2004 +http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, and +distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by the copyright +owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all other entities +that control, are controlled by, or are under common control with that entity. +For the purposes of this definition, "control" means (i) the power, direct or +indirect, to cause the direction or management of such entity, whether by +contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the +outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity exercising +permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, including +but not limited to software source code, documentation source, and configuration +files. + +"Object" form shall mean any form resulting from mechanical transformation or +translation of a Source form, including but not limited to compiled object code, +generated documentation, and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or Object form, made +available under the License, as indicated by a copyright notice that is included +in or attached to the work (an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object form, that +is based on (or derived from) the Work and for which the editorial revisions, +annotations, elaborations, or other modifications represent, as a whole, an +original work of authorship. For the purposes of this License, Derivative Works +shall not include works that remain separable from, or merely link (or bind by +name) to the interfaces of, the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including the original version +of the Work and any modifications or additions to that Work or Derivative Works +thereof, that is intentionally submitted to Licensor for inclusion in the Work +by the copyright owner or by an individual or Legal Entity authorized to submit +on behalf of the copyright owner. For the purposes of this definition, +"submitted" means any form of electronic, verbal, or written communication sent +to the Licensor or its representatives, including but not limited to +communication on electronic mailing lists, source code control systems, and +issue tracking systems that are managed by, or on behalf of, the Licensor for +the purpose of discussing and improving the Work, but excluding communication +that is conspicuously marked or otherwise designated in writing by the copyright +owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf +of whom a Contribution has been received by Licensor and subsequently +incorporated within the Work. + +2. Grant of Copyright License. + +Subject to the terms and conditions of this License, each Contributor hereby +grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, +irrevocable copyright license to reproduce, prepare Derivative Works of, +publicly display, publicly perform, sublicense, and distribute the Work and such +Derivative Works in Source or Object form. + +3. Grant of Patent License. + +Subject to the terms and conditions of this License, each Contributor hereby +grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, +irrevocable (except as stated in this section) patent license to make, have +made, use, offer to sell, sell, import, and otherwise transfer the Work, where +such license applies only to those patent claims licensable by such Contributor +that are necessarily infringed by their Contribution(s) alone or by combination +of their Contribution(s) with the Work to which such Contribution(s) was +submitted. If You institute patent litigation against any entity (including a +cross-claim or counterclaim in a lawsuit) alleging that the Work or a +Contribution incorporated within the Work constitutes direct or contributory +patent infringement, then any patent licenses granted to You under this License +for that Work shall terminate as of the date such litigation is filed. + +4. Redistribution. + +You may reproduce and distribute copies of the Work or Derivative Works thereof +in any medium, with or without modifications, and in Source or Object form, +provided that You meet the following conditions: + +You must give any other recipients of the Work or Derivative Works a copy of +this License; and +You must cause any modified files to carry prominent notices stating that You +changed the files; and +You must retain, in the Source form of any Derivative Works that You distribute, +all copyright, patent, trademark, and attribution notices from the Source form +of the Work, excluding those notices that do not pertain to any part of the +Derivative Works; and +If the Work includes a "NOTICE" text file as part of its distribution, then any +Derivative Works that You distribute must include a readable copy of the +attribution notices contained within such NOTICE file, excluding those notices +that do not pertain to any part of the Derivative Works, in at least one of the +following places: within a NOTICE text file distributed as part of the +Derivative Works; within the Source form or documentation, if provided along +with the Derivative Works; or, within a display generated by the Derivative +Works, if and wherever such third-party notices normally appear. The contents of +the NOTICE file are for informational purposes only and do not modify the +License. You may add Your own attribution notices within Derivative Works that +You distribute, alongside or as an addendum to the NOTICE text from the Work, +provided that such additional attribution notices cannot be construed as +modifying the License. +You may add Your own copyright statement to Your modifications and may provide +additional or different license terms and conditions for use, reproduction, or +distribution of Your modifications, or for any such Derivative Works as a whole, +provided Your use, reproduction, and distribution of the Work otherwise complies +with the conditions stated in this License. + +5. Submission of Contributions. + +Unless You explicitly state otherwise, any Contribution intentionally submitted +for inclusion in the Work by You to the Licensor shall be under the terms and +conditions of this License, without any additional terms or conditions. +Notwithstanding the above, nothing herein shall supersede or modify the terms of +any separate license agreement you may have executed with Licensor regarding +such Contributions. + +6. Trademarks. + +This License does not grant permission to use the trade names, trademarks, +service marks, or product names of the Licensor, except as required for +reasonable and customary use in describing the origin of the Work and +reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. + +Unless required by applicable law or agreed to in writing, Licensor provides the +Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, +including, without limitation, any warranties or conditions of TITLE, +NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are +solely responsible for determining the appropriateness of using or +redistributing the Work and assume any risks associated with Your exercise of +permissions under this License. + +8. Limitation of Liability. + +In no event and under no legal theory, whether in tort (including negligence), +contract, or otherwise, unless required by applicable law (such as deliberate +and grossly negligent acts) or agreed to in writing, shall any Contributor be +liable to You for damages, including any direct, indirect, special, incidental, +or consequential damages of any character arising as a result of this License or +out of the use or inability to use the Work (including but not limited to +damages for loss of goodwill, work stoppage, computer failure or malfunction, or +any and all other commercial damages or losses), even if such Contributor has +been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. + +While redistributing the Work or Derivative Works thereof, You may choose to +offer, and charge a fee for, acceptance of support, warranty, indemnity, or +other liability obligations and/or rights consistent with this License. However, +in accepting such obligations, You may act only on Your own behalf and on Your +sole responsibility, not on behalf of any other Contributor, and only if You +agree to indemnify, defend, and hold each Contributor harmless for any liability +incurred by, or claims asserted against, such Contributor by reason of your +accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work + +To apply the Apache License to your work, attach the following boilerplate +notice, with the fields enclosed by brackets "[]" replaced with your own +identifying information. (Don't include the brackets!) The text should be +enclosed in the appropriate comment syntax for the file format. We also +recommend that a file or class name and description of purpose be included on +the same "printed page" as the copyright notice for easier identification within +third-party archives. + + Copyright 2014 Unknwon + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +================================================================ + gopkg.in/yaml.v2 https://gopkg.in/yaml.v2 ---------------------------------------------------------------- diff --git a/Dockerfile.goreleaser b/Dockerfile.goreleaser index a09d75e14..ee9073116 100644 --- a/Dockerfile.goreleaser +++ b/Dockerfile.goreleaser @@ -1,4 +1,4 @@ -FROM golang:1.26.5-alpine AS build +FROM golang:1.27.1-alpine AS build ARG TARGETARCH @@ -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-03T07-13-05Z +ARG MC_AMD64_SHA256=cd7fcd449bb6b52e2eb727431ba6975b1e5d90df011a75869020ea9ac9e2b2a8 +ARG MC_ARM64_SHA256=7962afc37c3e60e5758b19e819cb62d2f340ee655fad7b067e2ac9bc5716c2e8 RUN apk add -U --no-cache \ ca-certificates \ diff --git a/Makefile b/Makefile index 96cfe0162..529913f85 100644 --- a/Makefile +++ b/Makefile @@ -4,7 +4,7 @@ LDFLAGS := $(shell go run buildscripts/gen-ldflags.go) GOOS ?= $(shell go env GOOS) GOARCH ?= $(shell go env GOARCH) -GOLANGCI_VERSION ?= v2.11.3 +GOLANGCI_VERSION ?= v2.13.1 VERSION ?= $(shell git describe --tags) REPO ?= docker.io/pgsty @@ -48,7 +48,7 @@ credits: ## regenerate CREDITS from the licenses of Go modules linked into the b check-gen: ## check for updated autogenerated files @go generate ./... >/dev/null - @go mod tidy -compat=1.26 + @go mod tidy -compat=1.27 @env bash $(PWD)/buildscripts/gen-credits.sh @changed=$$(git diff --name-only -- '*_gen.go' '*_gen_test.go' '*_msgp_test.go' '*_string.go' go.mod go.sum CREDITS); \ if [ -n "$$changed" ]; then \ diff --git a/README.md b/README.md index 4a58b2df3..644e31833 100644 --- a/README.md +++ b/README.md @@ -95,51 +95,52 @@ Report vulnerabilities privately as described in [`SECURITY.md`](SECURITY.md); e ## Contributors - - - - - - - -
- ZouhairCharef
@ZouhairCharef

CVE-2026-34986 -
- mfredenhagen
@mfredenhagen

CVE-2026-39883 -
- pinginfo
@pinginfo

Notification streaming -
- waterkip
@waterkip

Documentation links -
-

-magicxor -ycjlin -davinkevin -lem21h -sulin37392 -mosesdd -Xavier-777 -jiadzh -TLINDEN -AntonOfTheWoods -zylpsrs -nsanitate -makinikm -spaceg00se-r -heroes1412 -vampywiz17 -chalukyaj -cbornet -jvasile -Kesavaambati -redfoxfox -kuldeep-link11 -meesudzu -pmezhuev -kh0mka +**40 community contributors** build SILO, Console, mcli, shared packages, and related projects. The list includes maintainers and every human Issue or PR author, ordered by merged PRs, other PRs, then issue reports. Gold rings highlight significant contributions. + +

+@Vonng +@h5vx +@mrjavadseydi +@Dansyuqri +@ycjlin +@pinginfo +@ZouhairCharef +@mfredenhagen +@waterkip +@mikemikimike +@metaneutrons +@magicxor +@davinkevin +@lem21h +@sulin37392 +@cbornet +@vampywiz17 +@mumu-lab +@jvasile +@pmezhuev +@TLINDEN +@makinikm +@meesudzu +@kuldeep-link11 +@sargarass +@liuhaodongliu990-cmyk +@Xavier-777 +@spaceg00se-r +@kh0mka +@bagutzu +@DestroyLee +@mosesdd +@zylpsrs +@heroes1412 +@redfoxfox +@jiadzh +@AntonOfTheWoods +@chalukyaj +@nsanitate +@Kesavaambati

-GitHub does not generate a contributor graph for forks, so [`CONTRIBUTORS.md`](CONTRIBUTORS.md) — not the Insights page — is this project's attribution record. It names everyone alongside the change or report they contributed. +[View the full contribution record](CONTRIBUTORS.md) for each person's proposals, fixes, and reports. ## Background diff --git a/README_ZH.md b/README_ZH.md index dac446a6a..2075beabd 100644 --- a/README_ZH.md +++ b/README_ZH.md @@ -95,51 +95,52 @@ S3 API、`MINIO_*` 环境变量、`minio_*` 指标、`x-minio-*` 头、`/minio/* ## 贡献者 - - - - - - - -
- ZouhairCharef
@ZouhairCharef

CVE-2026-34986 -
- mfredenhagen
@mfredenhagen

CVE-2026-39883 -
- pinginfo
@pinginfo

桶通知流式输出 -
- waterkip
@waterkip

文档链接修正 -
-

-magicxor -ycjlin -davinkevin -lem21h -sulin37392 -mosesdd -Xavier-777 -jiadzh -TLINDEN -AntonOfTheWoods -zylpsrs -nsanitate -makinikm -spaceg00se-r -heroes1412 -vampywiz17 -chalukyaj -cbornet -jvasile -Kesavaambati -redfoxfox -kuldeep-link11 -meesudzu -pmezhuev -kh0mka +**40 位社区贡献者**共同建设 SILO、Console、mcli、公共包与相关项目。名单包含维护者,以及所有提出 Issue 或 PR 的真人作者;按已合并 PR、其他 PR、Issue 报告排序,黄圈标记显著贡献。 + +

+@Vonng +@h5vx +@mrjavadseydi +@Dansyuqri +@ycjlin +@pinginfo +@ZouhairCharef +@mfredenhagen +@waterkip +@mikemikimike +@metaneutrons +@magicxor +@davinkevin +@lem21h +@sulin37392 +@cbornet +@vampywiz17 +@mumu-lab +@jvasile +@pmezhuev +@TLINDEN +@makinikm +@meesudzu +@kuldeep-link11 +@sargarass +@liuhaodongliu990-cmyk +@Xavier-777 +@spaceg00se-r +@kh0mka +@bagutzu +@DestroyLee +@mosesdd +@zylpsrs +@heroes1412 +@redfoxfox +@jiadzh +@AntonOfTheWoods +@chalukyaj +@nsanitate +@Kesavaambati

-GitHub 不为 fork 仓库生成贡献者图表,因此 [`CONTRIBUTORS.md`](CONTRIBUTORS.md)(而非 Insights 页面)才是本项目的署名记录,其中逐一记录了每个人对应的改动或报告。 +[查看完整贡献记录](CONTRIBUTORS.md),了解每位贡献者的提案、修复与问题报告。 ## 背景 diff --git a/SECURITY.md b/SECURITY.md index fcd954b01..dcfd6e4ea 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -10,6 +10,25 @@ Security fixes are tracked on the active development branch and summarized in [docs/security/advisories.md](docs/security/advisories.md). Only the current Silo release line is supported unless an advisory says otherwise. +## Inherited Fix Evidence + +The canonical ledger also records security fixes inherited from upstream when +they are part of the Silo release baseline. Source and fork commits are linked +separately even when the fork preserves the original commit object and SHA. + +- [CVE-2025-62506](https://github.com/advisories/GHSA-jjjj-jwhf-8rgr): + upstream [PR #21642](https://github.com/minio/minio/pull/21642) merged as + [`minio/minio@c1a49490`](https://github.com/minio/minio/commit/c1a49490c78e9c3ebcad86ba0662319138ace190), + inherited unchanged as + [`pgsty/silo@c1a49490`](https://github.com/pgsty/silo/commit/c1a49490c78e9c3ebcad86ba0662319138ace190), + and is present in every Silo community release beginning with + [`RELEASE.2025-12-03T12-00-00Z`](https://github.com/pgsty/silo/releases/tag/RELEASE.2025-12-03T12-00-00Z). + The inherited [service-account](https://github.com/pgsty/silo/blob/c1a49490c78e9c3ebcad86ba0662319138ace190/cmd/admin-handlers-users_test.go#L211-L212) + and [STS](https://github.com/pgsty/silo/blob/c1a49490c78e9c3ebcad86ba0662319138ace190/cmd/sts-handlers_test.go#L45-L46) + regression groups remain part of `go test ./cmd`; see the + [canonical ledger](docs/security/advisories.md#inherited-upstream-advisory-baseline) + for the operator-facing record. + ## Reporting a Vulnerability For vulnerabilities in this fork: diff --git a/buildscripts/check-release-state.sh b/buildscripts/check-release-state.sh new file mode 100755 index 000000000..85dfb8ced --- /dev/null +++ b/buildscripts/check-release-state.sh @@ -0,0 +1,76 @@ +#!/usr/bin/env bash + +# Fail closed before a release job can replace published or finalized assets. +# An ordinary Draft is retry state; a finalized Draft contains GPG-derived +# materials and must never be replaced by the build lane. + +set -euo pipefail + +release_tag="${1:-}" +fixture="${2:-}" +repository="${GITHUB_REPOSITORY:-pgsty/silo}" +require_draft="${REQUIRE_DRAFT:-false}" + +if ! command -v jq >/dev/null 2>&1; then + echo "jq is required to inspect GitHub release state" >&2 + exit 1 +fi + +if [[ ! "${release_tag}" =~ ^RELEASE\.[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}-[0-9]{2}-[0-9]{2}Z$ ]]; then + echo "Invalid release tag format: ${release_tag:-}" >&2 + exit 1 +fi + +if [ -n "${fixture}" ]; then + release_json="$(<"${fixture}")" +else + error_file="$(mktemp)" + trap 'rm -f "${error_file}"' EXIT + if ! release_json="$( + gh api --paginate "repos/${repository}/releases?per_page=100" --jq '.[]' 2>"${error_file}" | + jq --arg tag "${release_tag}" -s '[.[] | select(.tag_name == $tag)]' + )"; then + cat "${error_file}" >&2 + exit 1 + fi +fi + +if ! jq -e 'type == "array" and all(.[]; type == "object" and (.tag_name | type == "string") and (.draft | type == "boolean"))' \ + <<<"${release_json}" >/dev/null 2>&1; then + echo "Invalid release state response for ${release_tag}" >&2 + exit 1 +fi + +if ! jq -e --arg tag "${release_tag}" 'all(.[]; .tag_name == $tag)' \ + <<<"${release_json}" >/dev/null 2>&1; then + echo "Release state returned a tag other than ${release_tag}" >&2 + exit 1 +fi + +release_count="$(jq 'length' <<<"${release_json}")" +if [ "${release_count}" -eq 0 ]; then + if [ "${require_draft}" = "true" ]; then + echo "Expected one Draft release for ${release_tag}, found none" >&2 + exit 1 + fi + echo "No existing release for ${release_tag}." + exit 0 +fi + +if [ "${release_count}" -ne 1 ]; then + echo "Refusing to choose among ${release_count} releases for ${release_tag}; clean duplicate Drafts first" >&2 + exit 1 +fi + +if [ "$(jq -r '.[0].draft' <<<"${release_json}")" != "true" ]; then + echo "Refusing to overwrite published release ${release_tag}" >&2 + exit 1 +fi + +finalize_markers="$(jq '[.[0].assets[]? | select(.name | endswith("_packages_provenance.sigstore.json"))] | length' <<<"${release_json}")" +if [ "${finalize_markers}" -ne 0 ]; then + echo "Refusing to replace finalized Draft ${release_tag}" >&2 + exit 1 +fi + +echo "Existing unfinalized Draft ${release_tag} will be replaced from scratch." diff --git a/buildscripts/check-release-state_test.sh b/buildscripts/check-release-state_test.sh new file mode 100755 index 000000000..6e2b63779 --- /dev/null +++ b/buildscripts/check-release-state_test.sh @@ -0,0 +1,68 @@ +#!/usr/bin/env bash + +set -euo pipefail + +script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +checker="${script_dir}/check-release-state.sh" +tag="RELEASE.2026-08-29T00-00-00Z" +fixture="$(mktemp)" +stdout_file="$(mktemp)" +stderr_file="$(mktemp)" +trap 'rm -f "${fixture}" "${stdout_file}" "${stderr_file}"' EXIT + +expect_success() { + if ! "${checker}" "$@" >"${stdout_file}" 2>"${stderr_file}"; then + cat "${stderr_file}" >&2 + return 1 + fi +} + +expect_failure() { + if "${checker}" "$@" >"${stdout_file}" 2>"${stderr_file}"; then + echo "Expected release-state check to fail: $*" >&2 + return 1 + fi +} + +printf '[]\n' >"${fixture}" +expect_success "${tag}" "${fixture}" +grep -qF "No existing release for ${tag}." "${stdout_file}" + +if REQUIRE_DRAFT=true "${checker}" "${tag}" "${fixture}" >"${stdout_file}" 2>"${stderr_file}"; then + echo "Expected required-Draft check to fail when no release exists" >&2 + exit 1 +fi +grep -qF "Expected one Draft release for ${tag}, found none" "${stderr_file}" + +printf '[{"tag_name":"%s","draft":true,"assets":[]}]\n' "${tag}" >"${fixture}" +expect_success "${tag}" "${fixture}" +grep -qF "Existing unfinalized Draft ${tag} will be replaced from scratch." "${stdout_file}" +if ! REQUIRE_DRAFT=true "${checker}" "${tag}" "${fixture}" >"${stdout_file}" 2>"${stderr_file}"; then + cat "${stderr_file}" >&2 + exit 1 +fi + +printf '[{"tag_name":"%s","draft":true,"assets":[{"name":"silo_20260829000000.0.0_packages_provenance.sigstore.json"}]}]\n' "${tag}" >"${fixture}" +expect_failure "${tag}" "${fixture}" +grep -qF "Refusing to replace finalized Draft ${tag}" "${stderr_file}" + +printf '[{"tag_name":"%s","draft":false}]\n' "${tag}" >"${fixture}" +expect_failure "${tag}" "${fixture}" +grep -qF "Refusing to overwrite published release ${tag}" "${stderr_file}" + +printf '[{"tag_name":"%s","draft":true},{"tag_name":"%s","draft":true}]\n' "${tag}" "${tag}" >"${fixture}" +expect_failure "${tag}" "${fixture}" +grep -qF "Refusing to choose among 2 releases" "${stderr_file}" + +printf '[{"tag_name":"RELEASE.2026-08-28T00-00-00Z","draft":true}]\n' >"${fixture}" +expect_failure "${tag}" "${fixture}" +grep -qF "other than ${tag}" "${stderr_file}" + +printf '{not-json}\n' >"${fixture}" +expect_failure "${tag}" "${fixture}" +grep -qF "Invalid release state response for ${tag}" "${stderr_file}" + +expect_failure "not-a-release-tag" "${fixture}" +grep -qF "Invalid release tag format" "${stderr_file}" + +echo "release-state decision tests passed" diff --git a/buildscripts/checkdeps.sh b/buildscripts/checkdeps.sh index ed4f666ea..58b222462 100755 --- a/buildscripts/checkdeps.sh +++ b/buildscripts/checkdeps.sh @@ -7,7 +7,7 @@ _init() { ## Minimum required versions for build dependencies GIT_VERSION="1.0" - GO_VERSION="1.16" + GO_VERSION="1.27.1" OSX_VERSION="10.8" KNAME=$(uname -s) ARCH=$(uname -m) diff --git a/buildscripts/heal-inconsistent-versions.sh b/buildscripts/heal-inconsistent-versions.sh index ffd776e57..c284d7383 100755 --- a/buildscripts/heal-inconsistent-versions.sh +++ b/buildscripts/heal-inconsistent-versions.sh @@ -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 diff --git a/buildscripts/install-mcli.sh b/buildscripts/install-mcli.sh index ba231e9b4..2cf7c0a1f 100755 --- a/buildscripts/install-mcli.sh +++ b/buildscripts/install-mcli.sh @@ -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-03T07-13-05Z} 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 @@ -74,7 +74,7 @@ expected=$(awk -v asset="${archive}" ' { name=$2 sub(/^\*/, "", name) - if (name == asset && $1 ~ /^[0-9a-fA-F]{64}$/) print tolower($1) + if (name == asset && length($1) == 64 && $1 ~ /^[0-9a-fA-F]+$/) print tolower($1) } ' "${tmp_dir}/${checksums}") if ! printf '%s\n' "${expected}" | grep -Eq '^[0-9a-f]{64}$'; then diff --git a/buildscripts/multipart-quorum-test.sh b/buildscripts/multipart-quorum-test.sh index 4551f4303..80716169a 100644 --- a/buildscripts/multipart-quorum-test.sh +++ b/buildscripts/multipart-quorum-test.sh @@ -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) diff --git a/buildscripts/rebrand-guard/compat-baseline.json b/buildscripts/rebrand-guard/compat-baseline.json index 63859b1fc..8ae0f32dc 100644 --- a/buildscripts/rebrand-guard/compat-baseline.json +++ b/buildscripts/rebrand-guard/compat-baseline.json @@ -1,5 +1,5 @@ { - "version": 3, + "version": 4, "module_path": "github.com/minio/minio", "minio_imports": [ "github.com/minio/cli", @@ -41,6 +41,7 @@ "github.com/minio/minio/internal/auth", "github.com/minio/minio/internal/bpool", "github.com/minio/minio/internal/bucket/bandwidth", + "github.com/minio/minio/internal/bucket/cors", "github.com/minio/minio/internal/bucket/encryption", "github.com/minio/minio/internal/bucket/lifecycle", "github.com/minio/minio/internal/bucket/object/lock", @@ -114,25 +115,6 @@ "github.com/minio/minio/internal/store", "github.com/minio/mux", "github.com/minio/pkg/v3", - "github.com/minio/pkg/v3/certs", - "github.com/minio/pkg/v3/console", - "github.com/minio/pkg/v3/ellipses", - "github.com/minio/pkg/v3/env", - "github.com/minio/pkg/v3/ldap", - "github.com/minio/pkg/v3/mimedb", - "github.com/minio/pkg/v3/net", - "github.com/minio/pkg/v3/policy", - "github.com/minio/pkg/v3/policy/condition", - "github.com/minio/pkg/v3/quick", - "github.com/minio/pkg/v3/randreader", - "github.com/minio/pkg/v3/sftp", - "github.com/minio/pkg/v3/sync/errgroup", - "github.com/minio/pkg/v3/sys", - "github.com/minio/pkg/v3/trie", - "github.com/minio/pkg/v3/wildcard", - "github.com/minio/pkg/v3/words", - "github.com/minio/pkg/v3/workers", - "github.com/minio/pkg/v3/xtime", "github.com/minio/selfupdate", "github.com/minio/simdjson-go", "github.com/minio/sio", @@ -410,18 +392,13 @@ "MINIO_NOTIFY_MQTT_TOPIC", "MINIO_NOTIFY_MQTT_USERNAME", "MINIO_NOTIFY_MYSQL_COMMENT", - "MINIO_NOTIFY_MYSQL_DATABASE", "MINIO_NOTIFY_MYSQL_DSN_STRING", "MINIO_NOTIFY_MYSQL_ENABLE", "MINIO_NOTIFY_MYSQL_FORMAT", - "MINIO_NOTIFY_MYSQL_HOST", "MINIO_NOTIFY_MYSQL_MAX_OPEN_CONNECTIONS", - "MINIO_NOTIFY_MYSQL_PASSWORD", - "MINIO_NOTIFY_MYSQL_PORT", "MINIO_NOTIFY_MYSQL_QUEUE_DIR", "MINIO_NOTIFY_MYSQL_QUEUE_LIMIT", "MINIO_NOTIFY_MYSQL_TABLE", - "MINIO_NOTIFY_MYSQL_USERNAME", "MINIO_NOTIFY_NATS_ADDRESS", "MINIO_NOTIFY_NATS_CERT_AUTHORITY", "MINIO_NOTIFY_NATS_CLIENT_CERT", @@ -455,17 +432,12 @@ "MINIO_NOTIFY_NSQ_TOPIC", "MINIO_NOTIFY_POSTGRES_COMMENT", "MINIO_NOTIFY_POSTGRES_CONNECTION_STRING", - "MINIO_NOTIFY_POSTGRES_DATABASE", "MINIO_NOTIFY_POSTGRES_ENABLE", "MINIO_NOTIFY_POSTGRES_FORMAT", - "MINIO_NOTIFY_POSTGRES_HOST", "MINIO_NOTIFY_POSTGRES_MAX_OPEN_CONNECTIONS", - "MINIO_NOTIFY_POSTGRES_PASSWORD", - "MINIO_NOTIFY_POSTGRES_PORT", "MINIO_NOTIFY_POSTGRES_QUEUE_DIR", "MINIO_NOTIFY_POSTGRES_QUEUE_LIMIT", "MINIO_NOTIFY_POSTGRES_TABLE", - "MINIO_NOTIFY_POSTGRES_USERNAME", "MINIO_NOTIFY_REDIS_ADDRESS", "MINIO_NOTIFY_REDIS_COMMENT", "MINIO_NOTIFY_REDIS_ENABLE", @@ -659,6 +631,7 @@ "x-minio-replication-encrypted-multipart", "x-minio-replication-ready", "x-minio-replication-reset-status", + "x-minio-replication-server-side-encryption", "x-minio-replication-server-side-encryption-iv", "x-minio-replication-server-side-encryption-seal-algorithm", "x-minio-replication-server-side-encryption-sealed-key", @@ -688,31 +661,9 @@ ], "routes": [ "/", - "/${filename}", - "/%s/us-east-1/s3/aws4_request", - "/*", - "/../../etc", - "/./abc/def", "/.dockerenv", "/.trash", "//", - "///abc", - "///object////", - "//123", - "//abc", - "//abc//", - "//contains/double-forwardslash-prefix", - "/?", - "/?X-Amz-Algorithm=AWS4-HMAC-SHA256\u0026X-Amz-Credential=USWUXHGYZQYFYFFIT3RE%2F20170529%2Fus-east-1%2Fs3%2Faws4_request\u0026X-Amz-Date=20170529T190139Z\u0026X-Amz-Expires=600\u0026X-Amz-Signature=19b58080999df54b446fc97304eb8dda60d3df1812ae97f3e8783351bfd9781d\u0026X-Amz-SignedHeaders=host\u0026prefix=Hello%2AWorld%2A", - "/a", - "/a/b/c", - "/a/b/c/d/e/f/g", - "/abc", - "/abc/", - "/abc/def", - "/abc/def/../..", - "/abc/def/../../..", - "/abcdef", "/accountinfo", "/add-canned-policy", "/add-service-account", @@ -723,15 +674,8 @@ "/apis", "/audit", "/background-heal/status", - "/bucket", "/bucket/", - "/bucket////object////", - "/bucket/a", "/bucket/api", - "/bucket/object", - "/bucket/object///////", - "/bucket/object/1/", - "/bucket/object/1///", "/bucket/replication", "/cancel-job", "/cfile", @@ -747,54 +691,21 @@ "/cluster/usage/objects", "/commitbinary", "/config", - "/config.json", "/crossdomain.xml", - "/d1", - "/d2", - "/d3", - "/d4", - "/data", "/datausageinfo", "/dblk", "/debug/go", "/del-config-kv", "/delete-service-account", "/describe-job", - "/dev/0", - "/dev/1", - "/dev/2", "/dev/disk/by-uuid/", "/devnull", - "/dir1", - "/dir1/dira", - "/disk", "/downloadprofilingdata", "/drivespeedtest", "/dvers", - "/etc/minio/certs", - "/export", "/export-bucket-metadata", "/export-iam", - "/export/set", - "/export/set{1...64}", - "/export/test{1...2O}", - "/export1", - "/export1{-1...1}", - "/export1{01...64}", - "/export1{1...32}", - "/export1{1...64}", - "/export1{33...64}", - "/export1{64...1}", - "/export1{a...z}", - "/export2", - "/export3", - "/export4", - "/export{1...10}/disk{1...10}", - "/export{1..2}", - "/file", - "/file/obj1", "/filename.txt", - "/foo", "/force-unlock", "/get-bucket-quota", "/get-config-kv", @@ -845,7 +756,6 @@ "/list-service-accounts", "/list-users", "/live", - "/local/skydns/staging/service", "/log", "/logger/webhook", "/ls", @@ -853,37 +763,13 @@ "/metrics/v3", "/minio/grid/", "/minio/grid/lock/", - "/minio/health/cluster", - "/minio/health/cluster/read", - "/minio/health/live", - "/minio/health/ready", - "/myobject*", "/netperf", - "/newfolder", - "/nonexistent-dir", - "/nonexistent/env-override.creds", - "/nonexistent/env-override.nk", - "/nonexistent/env-wins.creds", - "/nonexistent/new-key.creds", - "/nonexistent/test.creds", - "/nonexistent/test.nk", - "/nonexistying", "/notification", "/oauth2/callback", "/oauth_callback", "/obdinfo", - "/obj", - "/objectlambda/", - "/p/q/r/s/t", "/part", - "/path/to/0", - "/path/to/1", - "/path/to/1/2", - "/path/to/2", - "/path/to/x", "/peer", - "/peer-created", - "/peer-victim", "/podinfo/labels", "/policies/", "/policydb/", @@ -891,14 +777,12 @@ "/pools/decommission", "/pools/list", "/pools/status", - "/probe", "/proc/mounts", "/proc/self/mountinfo", "/profile", "/profiling/download", "/profiling/start", "/prometheus/metrics", - "/putbucket-.", "/rall", "/ready", "/rebalance/start", @@ -922,11 +806,8 @@ "/rver", "/rxl", "/scanner", - "/secret", - "/secret?", "/service", "/service-accounts/", - "/service?", "/set-bucket-quota", "/set-config-kv", "/set-group-status", @@ -934,8 +815,6 @@ "/set-user-or-group-policy", "/set-user-status", "/sfile", - "/singleleveldomain", - "/singleleveldomain/", "/site-replication", "/site-replication/add", "/site-replication/devnull", @@ -956,9 +835,6 @@ "/site-replication/state/edit", "/site-replication/status", "/skydns", - "/skydns/local/cluster/staging/service", - "/skydns/local/cluster/staging/service/", - "/source-bucket/source-object?", "/speedtest", "/speedtest/client/devnull", "/speedtest/client/devnull/extratime", @@ -983,32 +859,17 @@ "/system/network/internode", "/system/process", "/temporary-account-info", - "/test", - "/test/a/b/c", "/tier", "/tier-stats", "/tier/{tier}", "/tmp", - "/tmp/client.crt", - "/tmp/client.key", - "/tmp/drive1", - "/tmp/non-existent-directory", - "/tmp/non-existing-file", "/top/locks", "/trace", "/update", "/update-group-members", "/update-service-account", - "/upload.txt", "/user-info", "/users/", - "/v1/force-unlock", - "/v1/health", - "/v1/lock", - "/v1/refresh", - "/v1/rlock", - "/v1/runlock", - "/v1/unlock", "/v2/metrics/bucket", "/v2/metrics/cluster", "/v2/metrics/node", @@ -1107,9022 +968,6 @@ "arn:minio:sqs:us-east-1:444455556666:webhook", "minio:s3" ], - "exported_symbols": [ - "cmd:cmd:const:AdminUpdateApplyFailure", - "cmd:cmd:const:AdminUpdateURLNotReachable", - "cmd:cmd:const:AdminUpdateUnexpectedFailure", - "cmd:cmd:const:BLAKE2b512", - "cmd:cmd:const:BackendErasure", - "cmd:cmd:const:BackendFS", - "cmd:cmd:const:BatchJobExpireDeleted", - "cmd:cmd:const:BatchJobExpireObject", - "cmd:cmd:const:BatchJobReplicateResourceMinIO", - "cmd:cmd:const:BatchJobReplicateResourceS3", - "cmd:cmd:const:CounterMT", - "cmd:cmd:const:DefaultBitrotAlgorithm", - "cmd:cmd:const:DefaultSkewTime", - "cmd:cmd:const:DeleteType", - "cmd:cmd:const:Disabled", - "cmd:cmd:const:DistErasureSetupType", - "cmd:cmd:const:EnvErasureSetDriveCount", - "cmd:cmd:const:EnvPrometheusAuthType", - "cmd:cmd:const:EnvPrometheusOpenMetrics", - "cmd:cmd:const:ErasureSDSetupType", - "cmd:cmd:const:ErasureSetupType", - "cmd:cmd:const:ErrARNNotification", - "cmd:cmd:const:ErrAccessDenied", - "cmd:cmd:const:ErrAccessKeyDisabled", - "cmd:cmd:const:ErrAccountNotEligible", - "cmd:cmd:const:ErrAddUserInvalidArgument", - "cmd:cmd:const:ErrAddUserValidUTF", - "cmd:cmd:const:ErrAdminAccountNotEligible", - "cmd:cmd:const:ErrAdminBucketQuotaExceeded", - "cmd:cmd:const:ErrAdminConfigBadJSON", - "cmd:cmd:const:ErrAdminConfigDuplicateKeys", - "cmd:cmd:const:ErrAdminConfigEnvOverridden", - "cmd:cmd:const:ErrAdminConfigIDPCfgNameAlreadyExists", - "cmd:cmd:const:ErrAdminConfigIDPCfgNameDoesNotExist", - "cmd:cmd:const:ErrAdminConfigInvalidIDPType", - "cmd:cmd:const:ErrAdminConfigLDAPNonDefaultConfigName", - "cmd:cmd:const:ErrAdminConfigLDAPValidation", - "cmd:cmd:const:ErrAdminConfigNoQuorum", - "cmd:cmd:const:ErrAdminConfigNotificationTargetsFailed", - "cmd:cmd:const:ErrAdminConfigTooLarge", - "cmd:cmd:const:ErrAdminGroupDisabled", - "cmd:cmd:const:ErrAdminGroupNotEmpty", - "cmd:cmd:const:ErrAdminInvalidAccessKey", - "cmd:cmd:const:ErrAdminInvalidArgument", - "cmd:cmd:const:ErrAdminInvalidGroupName", - "cmd:cmd:const:ErrAdminInvalidSecretKey", - "cmd:cmd:const:ErrAdminLDAPExpectedLoginName", - "cmd:cmd:const:ErrAdminLDAPNotEnabled", - "cmd:cmd:const:ErrAdminNoAccessKey", - "cmd:cmd:const:ErrAdminNoSecretKey", - "cmd:cmd:const:ErrAdminNoSuchAccessKey", - "cmd:cmd:const:ErrAdminNoSuchConfigTarget", - "cmd:cmd:const:ErrAdminNoSuchGroup", - "cmd:cmd:const:ErrAdminNoSuchJob", - "cmd:cmd:const:ErrAdminNoSuchPolicy", - "cmd:cmd:const:ErrAdminNoSuchQuotaConfiguration", - "cmd:cmd:const:ErrAdminNoSuchUser", - "cmd:cmd:const:ErrAdminNoSuchUserLDAPWarn", - "cmd:cmd:const:ErrAdminOpenIDNotEnabled", - "cmd:cmd:const:ErrAdminPolicyChangeAlreadyApplied", - "cmd:cmd:const:ErrAdminProfilerNotEnabled", - "cmd:cmd:const:ErrAdminRebalanceAlreadyStarted", - "cmd:cmd:const:ErrAdminRebalanceNotStarted", - "cmd:cmd:const:ErrAdminResourceInvalidArgument", - "cmd:cmd:const:ErrAdminServiceAccountNotFound", - "cmd:cmd:const:ErrAllAccessDisabled", - "cmd:cmd:const:ErrAuthHeaderEmpty", - "cmd:cmd:const:ErrAuthorizationHeaderMalformed", - "cmd:cmd:const:ErrBackendDown", - "cmd:cmd:const:ErrBadDigest", - "cmd:cmd:const:ErrBadRequest", - "cmd:cmd:const:ErrBucketAlreadyExists", - "cmd:cmd:const:ErrBucketAlreadyOwnedByYou", - "cmd:cmd:const:ErrBucketMetadataNotInitialized", - "cmd:cmd:const:ErrBucketNotEmpty", - "cmd:cmd:const:ErrBucketRemoteAlreadyExists", - "cmd:cmd:const:ErrBucketRemoteArnInvalid", - "cmd:cmd:const:ErrBucketRemoteArnTypeInvalid", - "cmd:cmd:const:ErrBucketRemoteIdenticalToSource", - "cmd:cmd:const:ErrBucketRemoteLabelInUse", - "cmd:cmd:const:ErrBucketRemoteRemoveDisallowed", - "cmd:cmd:const:ErrBucketTaggingNotFound", - "cmd:cmd:const:ErrBusy", - "cmd:cmd:const:ErrCastFailed", - "cmd:cmd:const:ErrClientDisconnected", - "cmd:cmd:const:ErrContentChecksumMismatch", - "cmd:cmd:const:ErrContentSHA256Mismatch", - "cmd:cmd:const:ErrCredMalformed", - "cmd:cmd:const:ErrEmptyRequestBody", - "cmd:cmd:const:ErrEntityTooLarge", - "cmd:cmd:const:ErrEntityTooSmall", - "cmd:cmd:const:ErrEvaluatorBindingDoesNotExist", - "cmd:cmd:const:ErrEvaluatorInvalidArguments", - "cmd:cmd:const:ErrEvaluatorInvalidTimestampFormatPattern", - "cmd:cmd:const:ErrEvaluatorInvalidTimestampFormatPatternSymbol", - "cmd:cmd:const:ErrEvaluatorInvalidTimestampFormatPatternSymbolForParsing", - "cmd:cmd:const:ErrEvaluatorInvalidTimestampFormatPatternToken", - "cmd:cmd:const:ErrEvaluatorTimestampFormatPatternDuplicateFields", - "cmd:cmd:const:ErrEvaluatorTimestampFormatPatternHourClockAmPmMismatch", - "cmd:cmd:const:ErrEvaluatorUnterminatedTimestampFormatPatternToken", - "cmd:cmd:const:ErrEventNotification", - "cmd:cmd:const:ErrExcessData", - "cmd:cmd:const:ErrExpiredPresignRequest", - "cmd:cmd:const:ErrExpressionTooLong", - "cmd:cmd:const:ErrFilterNameInvalid", - "cmd:cmd:const:ErrFilterNamePrefix", - "cmd:cmd:const:ErrFilterNameSuffix", - "cmd:cmd:const:ErrFilterValueInvalid", - "cmd:cmd:const:ErrHealAlreadyRunning", - "cmd:cmd:const:ErrHealInvalidClientToken", - "cmd:cmd:const:ErrHealMissingBucket", - "cmd:cmd:const:ErrHealNoSuchProcess", - "cmd:cmd:const:ErrHealNotImplemented", - "cmd:cmd:const:ErrHealOverlappingPaths", - "cmd:cmd:const:ErrIAMNotInitialized", - "cmd:cmd:const:ErrIllegalSQLFunctionArgument", - "cmd:cmd:const:ErrIncompatibleEncryptionMethod", - "cmd:cmd:const:ErrIncompleteBody", - "cmd:cmd:const:ErrIncorrectContinuationToken", - "cmd:cmd:const:ErrIncorrectSQLFunctionArgumentType", - "cmd:cmd:const:ErrInsecureClientRequest", - "cmd:cmd:const:ErrInsecureSSECustomerRequest", - "cmd:cmd:const:ErrIntegerOverflow", - "cmd:cmd:const:ErrInternalError", - "cmd:cmd:const:ErrInvalidAccessKeyID", - "cmd:cmd:const:ErrInvalidArgument", - "cmd:cmd:const:ErrInvalidAttributeName", - "cmd:cmd:const:ErrInvalidBucketName", - "cmd:cmd:const:ErrInvalidBucketObjectLockConfiguration", - "cmd:cmd:const:ErrInvalidCast", - "cmd:cmd:const:ErrInvalidChecksum", - "cmd:cmd:const:ErrInvalidColumnIndex", - "cmd:cmd:const:ErrInvalidCompressionFormat", - "cmd:cmd:const:ErrInvalidCopyDest", - "cmd:cmd:const:ErrInvalidCopyPartRange", - "cmd:cmd:const:ErrInvalidCopyPartRangeSource", - "cmd:cmd:const:ErrInvalidCopySource", - "cmd:cmd:const:ErrInvalidDataSource", - "cmd:cmd:const:ErrInvalidDataType", - "cmd:cmd:const:ErrInvalidDecompressedSize", - "cmd:cmd:const:ErrInvalidDigest", - "cmd:cmd:const:ErrInvalidDuration", - "cmd:cmd:const:ErrInvalidEncodingMethod", - "cmd:cmd:const:ErrInvalidEncryptionKeyID", - "cmd:cmd:const:ErrInvalidEncryptionMethod", - "cmd:cmd:const:ErrInvalidEncryptionParameters", - "cmd:cmd:const:ErrInvalidEncryptionParametersSSEC", - "cmd:cmd:const:ErrInvalidExpressionType", - "cmd:cmd:const:ErrInvalidFileHeaderInfo", - "cmd:cmd:const:ErrInvalidJSONType", - "cmd:cmd:const:ErrInvalidKeyPath", - "cmd:cmd:const:ErrInvalidLifecycleQueryParameter", - "cmd:cmd:const:ErrInvalidLifecycleWithObjectLock", - "cmd:cmd:const:ErrInvalidMaxKeys", - "cmd:cmd:const:ErrInvalidMaxParts", - "cmd:cmd:const:ErrInvalidMaxUploads", - "cmd:cmd:const:ErrInvalidMetadataDirective", - "cmd:cmd:const:ErrInvalidObjectName", - "cmd:cmd:const:ErrInvalidObjectNamePrefixSlash", - "cmd:cmd:const:ErrInvalidObjectState", - "cmd:cmd:const:ErrInvalidPart", - "cmd:cmd:const:ErrInvalidPartNumber", - "cmd:cmd:const:ErrInvalidPartNumberMarker", - "cmd:cmd:const:ErrInvalidPartOrder", - "cmd:cmd:const:ErrInvalidPolicyDocument", - "cmd:cmd:const:ErrInvalidPrefixMarker", - "cmd:cmd:const:ErrInvalidQueryParams", - "cmd:cmd:const:ErrInvalidQuerySignatureAlgo", - "cmd:cmd:const:ErrInvalidQuoteFields", - "cmd:cmd:const:ErrInvalidRange", - "cmd:cmd:const:ErrInvalidRangePartNumber", - "cmd:cmd:const:ErrInvalidRegion", - "cmd:cmd:const:ErrInvalidRequest", - "cmd:cmd:const:ErrInvalidRequestBody", - "cmd:cmd:const:ErrInvalidRequestParameter", - "cmd:cmd:const:ErrInvalidRequestVersion", - "cmd:cmd:const:ErrInvalidResourceName", - "cmd:cmd:const:ErrInvalidRetentionDate", - "cmd:cmd:const:ErrInvalidSSECustomerAlgorithm", - "cmd:cmd:const:ErrInvalidSSECustomerKey", - "cmd:cmd:const:ErrInvalidSSECustomerParameters", - "cmd:cmd:const:ErrInvalidServiceS3", - "cmd:cmd:const:ErrInvalidServiceSTS", - "cmd:cmd:const:ErrInvalidStorageClass", - "cmd:cmd:const:ErrInvalidTableAlias", - "cmd:cmd:const:ErrInvalidTagDirective", - "cmd:cmd:const:ErrInvalidTextEncoding", - "cmd:cmd:const:ErrInvalidToken", - "cmd:cmd:const:ErrInvalidVersionID", - "cmd:cmd:const:ErrKMSDefaultKeyAlreadyConfigured", - "cmd:cmd:const:ErrKMSKeyNotFoundException", - "cmd:cmd:const:ErrKMSNotConfigured", - "cmd:cmd:const:ErrKeyTooLongError", - "cmd:cmd:const:ErrLambdaARNInvalid", - "cmd:cmd:const:ErrLambdaARNNotFound", - "cmd:cmd:const:ErrLexerInvalidChar", - "cmd:cmd:const:ErrLexerInvalidIONLiteral", - "cmd:cmd:const:ErrLexerInvalidLiteral", - "cmd:cmd:const:ErrLexerInvalidOperator", - "cmd:cmd:const:ErrLikeInvalidInputs", - "cmd:cmd:const:ErrMalformedCredentialDate", - "cmd:cmd:const:ErrMalformedDate", - "cmd:cmd:const:ErrMalformedExpires", - "cmd:cmd:const:ErrMalformedJSON", - "cmd:cmd:const:ErrMalformedPOSTRequest", - "cmd:cmd:const:ErrMalformedPresignedDate", - "cmd:cmd:const:ErrMalformedXML", - "cmd:cmd:const:ErrMaxVersionsExceeded", - "cmd:cmd:const:ErrMaximumExpires", - "cmd:cmd:const:ErrMetadataTooLarge", - "cmd:cmd:const:ErrMethodNotAllowed", - "cmd:cmd:const:ErrMissingContentLength", - "cmd:cmd:const:ErrMissingContentMD5", - "cmd:cmd:const:ErrMissingCredTag", - "cmd:cmd:const:ErrMissingDateHeader", - "cmd:cmd:const:ErrMissingFields", - "cmd:cmd:const:ErrMissingHeaders", - "cmd:cmd:const:ErrMissingPart", - "cmd:cmd:const:ErrMissingRequestBodyError", - "cmd:cmd:const:ErrMissingRequiredParameter", - "cmd:cmd:const:ErrMissingSSECustomerKey", - "cmd:cmd:const:ErrMissingSSECustomerKeyMD5", - "cmd:cmd:const:ErrMissingSecurityHeader", - "cmd:cmd:const:ErrMissingSignHeadersTag", - "cmd:cmd:const:ErrMissingSignTag", - "cmd:cmd:const:ErrNegativeExpires", - "cmd:cmd:const:ErrNoAccessKey", - "cmd:cmd:const:ErrNoSuchBucket", - "cmd:cmd:const:ErrNoSuchBucketLifecycle", - "cmd:cmd:const:ErrNoSuchBucketPolicy", - "cmd:cmd:const:ErrNoSuchBucketSSEConfig", - "cmd:cmd:const:ErrNoSuchCORSConfiguration", - "cmd:cmd:const:ErrNoSuchKey", - "cmd:cmd:const:ErrNoSuchLifecycleConfiguration", - "cmd:cmd:const:ErrNoSuchObjectLockConfiguration", - "cmd:cmd:const:ErrNoSuchUpload", - "cmd:cmd:const:ErrNoSuchVersion", - "cmd:cmd:const:ErrNoSuchWebsiteConfiguration", - "cmd:cmd:const:ErrNoTokenRevokeType", - "cmd:cmd:const:ErrNone", - "cmd:cmd:const:ErrNotImplemented", - "cmd:cmd:const:ErrObjectExistsAsDirectory", - "cmd:cmd:const:ErrObjectLockConfigurationNotAllowed", - "cmd:cmd:const:ErrObjectLockConfigurationNotFound", - "cmd:cmd:const:ErrObjectLockInvalidHeaders", - "cmd:cmd:const:ErrObjectLocked", - "cmd:cmd:const:ErrObjectRestoreAlreadyInProgress", - "cmd:cmd:const:ErrObjectSerializationConflict", - "cmd:cmd:const:ErrObjectTampered", - "cmd:cmd:const:ErrOverlappingConfigs", - "cmd:cmd:const:ErrOverlappingFilterNotification", - "cmd:cmd:const:ErrPOSTFileRequired", - "cmd:cmd:const:ErrParseAsteriskIsNotAloneInSelectList", - "cmd:cmd:const:ErrParseCannotMixSqbAndWildcardInSelectList", - "cmd:cmd:const:ErrParseCastArity", - "cmd:cmd:const:ErrParseEmptySelect", - "cmd:cmd:const:ErrParseExpected2TokenTypes", - "cmd:cmd:const:ErrParseExpectedArgumentDelimiter", - "cmd:cmd:const:ErrParseExpectedDatePart", - "cmd:cmd:const:ErrParseExpectedExpression", - "cmd:cmd:const:ErrParseExpectedIdentForAlias", - "cmd:cmd:const:ErrParseExpectedIdentForAt", - "cmd:cmd:const:ErrParseExpectedIdentForGroupName", - "cmd:cmd:const:ErrParseExpectedKeyword", - "cmd:cmd:const:ErrParseExpectedLeftParenAfterCast", - "cmd:cmd:const:ErrParseExpectedLeftParenBuiltinFunctionCall", - "cmd:cmd:const:ErrParseExpectedLeftParenValueConstructor", - "cmd:cmd:const:ErrParseExpectedMember", - "cmd:cmd:const:ErrParseExpectedNumber", - "cmd:cmd:const:ErrParseExpectedRightParenBuiltinFunctionCall", - "cmd:cmd:const:ErrParseExpectedTokenType", - "cmd:cmd:const:ErrParseExpectedTypeName", - "cmd:cmd:const:ErrParseExpectedWhenClause", - "cmd:cmd:const:ErrParseInvalidContextForWildcardInSelectList", - "cmd:cmd:const:ErrParseInvalidTypeParam", - "cmd:cmd:const:ErrParseMalformedJoin", - "cmd:cmd:const:ErrParseMissingIdentAfterAt", - "cmd:cmd:const:ErrParseNonUnaryAggregateFunctionCall", - "cmd:cmd:const:ErrParseSelectMissingFrom", - "cmd:cmd:const:ErrParseUnexpectedKeyword", - "cmd:cmd:const:ErrParseUnexpectedOperator", - "cmd:cmd:const:ErrParseUnexpectedTerm", - "cmd:cmd:const:ErrParseUnexpectedToken", - "cmd:cmd:const:ErrParseUnknownOperator", - "cmd:cmd:const:ErrParseUnsupportedAlias", - "cmd:cmd:const:ErrParseUnsupportedCallWithStar", - "cmd:cmd:const:ErrParseUnsupportedCase", - "cmd:cmd:const:ErrParseUnsupportedCaseClause", - "cmd:cmd:const:ErrParseUnsupportedLiteralsGroupBy", - "cmd:cmd:const:ErrParseUnsupportedSelect", - "cmd:cmd:const:ErrParseUnsupportedSyntax", - "cmd:cmd:const:ErrParseUnsupportedToken", - "cmd:cmd:const:ErrPastObjectLockRetainDate", - "cmd:cmd:const:ErrPolicyAlreadyAttached", - "cmd:cmd:const:ErrPolicyInvalidName", - "cmd:cmd:const:ErrPolicyInvalidVersion", - "cmd:cmd:const:ErrPolicyNotAttached", - "cmd:cmd:const:ErrPolicyTooLarge", - "cmd:cmd:const:ErrPostPolicyConditionInvalidFormat", - "cmd:cmd:const:ErrPreconditionFailed", - "cmd:cmd:const:ErrRegionNotification", - "cmd:cmd:const:ErrRemoteDestinationNotFoundError", - "cmd:cmd:const:ErrRemoteTargetDenyAddError", - "cmd:cmd:const:ErrRemoteTargetNotFoundError", - "cmd:cmd:const:ErrRemoteTargetNotVersionedError", - "cmd:cmd:const:ErrReplicationBandwidthLimitError", - "cmd:cmd:const:ErrReplicationBucketNeedsVersioningError", - "cmd:cmd:const:ErrReplicationConfigurationNotFoundError", - "cmd:cmd:const:ErrReplicationDenyEditError", - "cmd:cmd:const:ErrReplicationDestinationMissingLock", - "cmd:cmd:const:ErrReplicationNeedsVersioningError", - "cmd:cmd:const:ErrReplicationNoExistingObjects", - "cmd:cmd:const:ErrReplicationPermissionCheckError", - "cmd:cmd:const:ErrReplicationRemoteConnectionError", - "cmd:cmd:const:ErrReplicationSourceNotVersionedError", - "cmd:cmd:const:ErrReplicationValidationError", - "cmd:cmd:const:ErrRequestBodyParse", - "cmd:cmd:const:ErrRequestNotReadyYet", - "cmd:cmd:const:ErrRequestTimeTooSkewed", - "cmd:cmd:const:ErrRequestTimedout", - "cmd:cmd:const:ErrSSECustomerKeyMD5Mismatch", - "cmd:cmd:const:ErrSSEEncryptedObject", - "cmd:cmd:const:ErrSSEMultipartEncrypted", - "cmd:cmd:const:ErrSTSAccessDenied", - "cmd:cmd:const:ErrSTSClientGrantsExpiredToken", - "cmd:cmd:const:ErrSTSIAMNotInitialized", - "cmd:cmd:const:ErrSTSInsecureConnection", - "cmd:cmd:const:ErrSTSInternalError", - "cmd:cmd:const:ErrSTSInvalidClientCertificate", - "cmd:cmd:const:ErrSTSInvalidClientGrantsToken", - "cmd:cmd:const:ErrSTSInvalidParameterValue", - "cmd:cmd:const:ErrSTSMalformedPolicyDocument", - "cmd:cmd:const:ErrSTSMissingParameter", - "cmd:cmd:const:ErrSTSNone", - "cmd:cmd:const:ErrSTSNotInitialized", - "cmd:cmd:const:ErrSTSTooManyIntermediateCAs", - "cmd:cmd:const:ErrSTSUpstreamError", - "cmd:cmd:const:ErrSTSWebIdentityExpiredToken", - "cmd:cmd:const:ErrServerNotInitialized", - "cmd:cmd:const:ErrSignatureDoesNotMatch", - "cmd:cmd:const:ErrSignatureVersionNotSupported", - "cmd:cmd:const:ErrSiteReplicationBackendIssue", - "cmd:cmd:const:ErrSiteReplicationBucketConfigError", - "cmd:cmd:const:ErrSiteReplicationBucketMetaError", - "cmd:cmd:const:ErrSiteReplicationConfigMissing", - "cmd:cmd:const:ErrSiteReplicationIAMConfigMismatch", - "cmd:cmd:const:ErrSiteReplicationIAMError", - "cmd:cmd:const:ErrSiteReplicationInvalidRequest", - "cmd:cmd:const:ErrSiteReplicationPeerResp", - "cmd:cmd:const:ErrSiteReplicationServiceAccountError", - "cmd:cmd:const:ErrSlowDownRead", - "cmd:cmd:const:ErrSlowDownWrite", - "cmd:cmd:const:ErrStorageFull", - "cmd:cmd:const:ErrTooManyRequests", - "cmd:cmd:const:ErrTransitionStorageClassNotFoundError", - "cmd:cmd:const:ErrUnauthorizedAccess", - "cmd:cmd:const:ErrUnknownWORMModeDirective", - "cmd:cmd:const:ErrUnsignedHeaders", - "cmd:cmd:const:ErrUnsupportedFunction", - "cmd:cmd:const:ErrUnsupportedHostHeader", - "cmd:cmd:const:ErrUnsupportedMetadata", - "cmd:cmd:const:ErrUnsupportedNotification", - "cmd:cmd:const:ErrUnsupportedRangeHeader", - "cmd:cmd:const:ErrUnsupportedSQLOperation", - "cmd:cmd:const:ErrUnsupportedSQLStructure", - "cmd:cmd:const:ErrUnsupportedSyntax", - "cmd:cmd:const:ErrValueParseFailure", - "cmd:cmd:const:FSSetupType", - "cmd:cmd:const:GaugeMT", - "cmd:cmd:const:GlobalMinioDefaultPort", - "cmd:cmd:const:GlobalStaleUploadsCleanupInterval", - "cmd:cmd:const:GlobalStaleUploadsExpiry", - "cmd:cmd:const:HighwayHash", - "cmd:cmd:const:HighwayHash256", - "cmd:cmd:const:HighwayHash256S", - "cmd:cmd:const:HistogramMT", - "cmd:cmd:const:ILMExpiry", - "cmd:cmd:const:ILMFreeVersionDelete", - "cmd:cmd:const:ILMTransition", - "cmd:cmd:const:LDAPUsersSysType", - "cmd:cmd:const:Large", - "cmd:cmd:const:LargeWorkerCount", - "cmd:cmd:const:LegacyType", - "cmd:cmd:const:MRFWorkerAutoDefault", - "cmd:cmd:const:MRFWorkerMaxLimit", - "cmd:cmd:const:MRFWorkerMinLimit", - "cmd:cmd:const:MarkDelete", - "cmd:cmd:const:MinIOUsersSysType", - "cmd:cmd:const:NoOp", - "cmd:cmd:const:NoResync", - "cmd:cmd:const:ObjectLockLegalHoldTimestamp", - "cmd:cmd:const:ObjectLockRetentionTimestamp", - "cmd:cmd:const:ObjectType", - "cmd:cmd:const:PathEndpointType", - "cmd:cmd:const:Purge", - "cmd:cmd:const:RQInconsistentMeta", - "cmd:cmd:const:RQInsufficientOnlineDrives", - "cmd:cmd:const:ReedSolomon", - "cmd:cmd:const:ReplicaStatus", - "cmd:cmd:const:ReplicaTimestamp", - "cmd:cmd:const:ReplicateDeleteAPI", - "cmd:cmd:const:ReplicateExisting", - "cmd:cmd:const:ReplicateExistingDelete", - "cmd:cmd:const:ReplicateHeal", - "cmd:cmd:const:ReplicateHealDelete", - "cmd:cmd:const:ReplicateIncoming", - "cmd:cmd:const:ReplicateIncomingDelete", - "cmd:cmd:const:ReplicateMRF", - "cmd:cmd:const:ReplicateObjectAPI", - "cmd:cmd:const:ReplicateQueued", - "cmd:cmd:const:ReplicationReset", - "cmd:cmd:const:ReplicationSsecChecksumHeader", - "cmd:cmd:const:ReplicationStatus", - "cmd:cmd:const:ReplicationTimestamp", - "cmd:cmd:const:ReservedMetadataPrefix", - "cmd:cmd:const:ReservedMetadataPrefixLower", - "cmd:cmd:const:ResyncCanceled", - "cmd:cmd:const:ResyncCompleted", - "cmd:cmd:const:ResyncFailed", - "cmd:cmd:const:ResyncPending", - "cmd:cmd:const:ResyncStarted", - "cmd:cmd:const:SHA256", - "cmd:cmd:const:SSECustomerKeySize", - "cmd:cmd:const:SSEDAREPackageBlockSize", - "cmd:cmd:const:SSEDAREPackageMetaSize", - "cmd:cmd:const:SSEIVSize", - "cmd:cmd:const:SelectRestoreRequest", - "cmd:cmd:const:SlashSeparator", - "cmd:cmd:const:SlashSeparatorChar", - "cmd:cmd:const:Small", - "cmd:cmd:const:TaggingTimestamp", - "cmd:cmd:const:Total", - "cmd:cmd:const:TransitionStatus", - "cmd:cmd:const:TransitionTier", - "cmd:cmd:const:TransitionedObjectName", - "cmd:cmd:const:TransitionedVersionID", - "cmd:cmd:const:URLEndpointType", - "cmd:cmd:const:Unknown", - "cmd:cmd:const:UnknownSetupType", - "cmd:cmd:const:VersionPurgeStatusKey", - "cmd:cmd:const:WalkVersionsSortAsc", - "cmd:cmd:const:WalkVersionsSortDesc", - "cmd:cmd:const:WorkerAutoDefault", - "cmd:cmd:const:WorkerMaxLimit", - "cmd:cmd:const:WorkerMinLimit", - "cmd:cmd:field:APIError.Code", - "cmd:cmd:field:APIError.Description", - "cmd:cmd:field:APIError.HTTPStatusCode", - "cmd:cmd:field:APIError.ObjectSize", - "cmd:cmd:field:APIError.RangeRequested", - "cmd:cmd:field:APIErrorResponse.ActualObjectSize", - "cmd:cmd:field:APIErrorResponse.BucketName", - "cmd:cmd:field:APIErrorResponse.Code", - "cmd:cmd:field:APIErrorResponse.HostID", - "cmd:cmd:field:APIErrorResponse.Key", - "cmd:cmd:field:APIErrorResponse.Message", - "cmd:cmd:field:APIErrorResponse.RangeRequested", - "cmd:cmd:field:APIErrorResponse.Region", - "cmd:cmd:field:APIErrorResponse.RequestID", - "cmd:cmd:field:APIErrorResponse.Resource", - "cmd:cmd:field:APIErrorResponse.XMLName", - "cmd:cmd:field:AccElem.N", - "cmd:cmd:field:AccElem.Size", - "cmd:cmd:field:AccElem.Total", - "cmd:cmd:field:ActiveWorkerStat.Avg", - "cmd:cmd:field:ActiveWorkerStat.Curr", - "cmd:cmd:field:ActiveWorkerStat.Max", - "cmd:cmd:field:AdminError.Code", - "cmd:cmd:field:AdminError.Message", - "cmd:cmd:field:AdminError.StatusCode", - "cmd:cmd:field:AssumeRoleResponse.ResponseMetadata", - "cmd:cmd:field:AssumeRoleResponse.Result", - "cmd:cmd:field:AssumeRoleResponse.XMLName", - "cmd:cmd:field:AssumeRoleResult.AssumedRoleUser", - "cmd:cmd:field:AssumeRoleResult.Credentials", - "cmd:cmd:field:AssumeRoleResult.PackedPolicySize", - "cmd:cmd:field:AssumeRoleWithCertificateResponse.Metadata", - "cmd:cmd:field:AssumeRoleWithCertificateResponse.Result", - "cmd:cmd:field:AssumeRoleWithCertificateResponse.XMLName", - "cmd:cmd:field:AssumeRoleWithClientGrantsResponse.ResponseMetadata", - "cmd:cmd:field:AssumeRoleWithClientGrantsResponse.Result", - "cmd:cmd:field:AssumeRoleWithClientGrantsResponse.XMLName", - "cmd:cmd:field:AssumeRoleWithCustomTokenResponse.Metadata", - "cmd:cmd:field:AssumeRoleWithCustomTokenResponse.Result", - "cmd:cmd:field:AssumeRoleWithCustomTokenResponse.XMLName", - "cmd:cmd:field:AssumeRoleWithLDAPResponse.ResponseMetadata", - "cmd:cmd:field:AssumeRoleWithLDAPResponse.Result", - "cmd:cmd:field:AssumeRoleWithLDAPResponse.XMLName", - "cmd:cmd:field:AssumeRoleWithWebIdentityResponse.ResponseMetadata", - "cmd:cmd:field:AssumeRoleWithWebIdentityResponse.Result", - "cmd:cmd:field:AssumeRoleWithWebIdentityResponse.XMLName", - "cmd:cmd:field:AssumedRoleUser.Arn", - "cmd:cmd:field:AssumedRoleUser.AssumedRoleID", - "cmd:cmd:field:AuditLogOptions.APIName", - "cmd:cmd:field:AuditLogOptions.Bucket", - "cmd:cmd:field:AuditLogOptions.Error", - "cmd:cmd:field:AuditLogOptions.Event", - "cmd:cmd:field:AuditLogOptions.Object", - "cmd:cmd:field:AuditLogOptions.Status", - "cmd:cmd:field:AuditLogOptions.Tags", - "cmd:cmd:field:AuditLogOptions.VersionID", - "cmd:cmd:field:BackendDown.Err", - "cmd:cmd:field:BatchJobExpire.APIVersion", - "cmd:cmd:field:BatchJobExpire.Bucket", - "cmd:cmd:field:BatchJobExpire.NotificationCfg", - "cmd:cmd:field:BatchJobExpire.Prefix", - "cmd:cmd:field:BatchJobExpire.Retry", - "cmd:cmd:field:BatchJobExpire.Rules", - "cmd:cmd:field:BatchJobExpireFilter.CreatedBefore", - "cmd:cmd:field:BatchJobExpireFilter.Metadata", - "cmd:cmd:field:BatchJobExpireFilter.Name", - "cmd:cmd:field:BatchJobExpireFilter.OlderThan", - "cmd:cmd:field:BatchJobExpireFilter.Purge", - "cmd:cmd:field:BatchJobExpireFilter.Size", - "cmd:cmd:field:BatchJobExpireFilter.Tags", - "cmd:cmd:field:BatchJobExpireFilter.Type", - "cmd:cmd:field:BatchJobExpirePurge.RetainVersions", - "cmd:cmd:field:BatchJobKV.Key", - "cmd:cmd:field:BatchJobKV.Value", - "cmd:cmd:field:BatchJobKeyRotateEncryption.Context", - "cmd:cmd:field:BatchJobKeyRotateEncryption.Key", - "cmd:cmd:field:BatchJobKeyRotateEncryption.Type", - "cmd:cmd:field:BatchJobKeyRotateFlags.Filter", - "cmd:cmd:field:BatchJobKeyRotateFlags.Notify", - "cmd:cmd:field:BatchJobKeyRotateFlags.Retry", - "cmd:cmd:field:BatchJobKeyRotateV1.APIVersion", - "cmd:cmd:field:BatchJobKeyRotateV1.Bucket", - "cmd:cmd:field:BatchJobKeyRotateV1.Encryption", - "cmd:cmd:field:BatchJobKeyRotateV1.Flags", - "cmd:cmd:field:BatchJobKeyRotateV1.Prefix", - "cmd:cmd:field:BatchJobNotification.Endpoint", - "cmd:cmd:field:BatchJobNotification.Token", - "cmd:cmd:field:BatchJobReplicateCredentials.AccessKey", - "cmd:cmd:field:BatchJobReplicateCredentials.SecretKey", - "cmd:cmd:field:BatchJobReplicateCredentials.SessionToken", - "cmd:cmd:field:BatchJobReplicateFlags.Filter", - "cmd:cmd:field:BatchJobReplicateFlags.Notify", - "cmd:cmd:field:BatchJobReplicateFlags.Retry", - "cmd:cmd:field:BatchJobReplicateSource.Bucket", - "cmd:cmd:field:BatchJobReplicateSource.Creds", - "cmd:cmd:field:BatchJobReplicateSource.Endpoint", - "cmd:cmd:field:BatchJobReplicateSource.Path", - "cmd:cmd:field:BatchJobReplicateSource.Prefix", - "cmd:cmd:field:BatchJobReplicateSource.Snowball", - "cmd:cmd:field:BatchJobReplicateSource.Type", - "cmd:cmd:field:BatchJobReplicateTarget.Bucket", - "cmd:cmd:field:BatchJobReplicateTarget.Creds", - "cmd:cmd:field:BatchJobReplicateTarget.Endpoint", - "cmd:cmd:field:BatchJobReplicateTarget.Path", - "cmd:cmd:field:BatchJobReplicateTarget.Prefix", - "cmd:cmd:field:BatchJobReplicateTarget.Type", - "cmd:cmd:field:BatchJobReplicateV1.APIVersion", - "cmd:cmd:field:BatchJobReplicateV1.Flags", - "cmd:cmd:field:BatchJobReplicateV1.Source", - "cmd:cmd:field:BatchJobReplicateV1.Target", - "cmd:cmd:field:BatchJobRequest.Expire", - "cmd:cmd:field:BatchJobRequest.ID", - "cmd:cmd:field:BatchJobRequest.KeyRotate", - "cmd:cmd:field:BatchJobRequest.Replicate", - "cmd:cmd:field:BatchJobRequest.Started", - "cmd:cmd:field:BatchJobRequest.User", - "cmd:cmd:field:BatchJobRetry.Attempts", - "cmd:cmd:field:BatchJobRetry.Delay", - "cmd:cmd:field:BatchJobSizeFilter.LowerBound", - "cmd:cmd:field:BatchJobSizeFilter.UpperBound", - "cmd:cmd:field:BatchJobSnowball.Batch", - "cmd:cmd:field:BatchJobSnowball.Compress", - "cmd:cmd:field:BatchJobSnowball.Disable", - "cmd:cmd:field:BatchJobSnowball.InMemory", - "cmd:cmd:field:BatchJobSnowball.SkipErrs", - "cmd:cmd:field:BatchJobSnowball.SmallerThan", - "cmd:cmd:field:BatchKeyRotateFilter.CreatedAfter", - "cmd:cmd:field:BatchKeyRotateFilter.CreatedBefore", - "cmd:cmd:field:BatchKeyRotateFilter.KMSKeyID", - "cmd:cmd:field:BatchKeyRotateFilter.Metadata", - "cmd:cmd:field:BatchKeyRotateFilter.NewerThan", - "cmd:cmd:field:BatchKeyRotateFilter.OlderThan", - "cmd:cmd:field:BatchKeyRotateFilter.Tags", - "cmd:cmd:field:BatchKeyRotateNotification.Endpoint", - "cmd:cmd:field:BatchKeyRotateNotification.Token", - "cmd:cmd:field:BatchReplicateFilter.CreatedAfter", - "cmd:cmd:field:BatchReplicateFilter.CreatedBefore", - "cmd:cmd:field:BatchReplicateFilter.Metadata", - "cmd:cmd:field:BatchReplicateFilter.NewerThan", - "cmd:cmd:field:BatchReplicateFilter.OlderThan", - "cmd:cmd:field:BatchReplicateFilter.Tags", - "cmd:cmd:field:Bucket.CreationDate", - "cmd:cmd:field:Bucket.Name", - "cmd:cmd:field:BucketAccessPolicy.Bucket", - "cmd:cmd:field:BucketAccessPolicy.Policy", - "cmd:cmd:field:BucketAccessPolicy.Prefix", - "cmd:cmd:field:BucketInfo.Created", - "cmd:cmd:field:BucketInfo.Deleted", - "cmd:cmd:field:BucketInfo.Name", - "cmd:cmd:field:BucketInfo.ObjectLocking", - "cmd:cmd:field:BucketInfo.Versioning", - "cmd:cmd:field:BucketMetadata.BucketTargetsConfigJSON", - "cmd:cmd:field:BucketMetadata.BucketTargetsConfigMetaJSON", - "cmd:cmd:field:BucketMetadata.BucketTargetsConfigMetaUpdatedAt", - "cmd:cmd:field:BucketMetadata.BucketTargetsConfigUpdatedAt", - "cmd:cmd:field:BucketMetadata.Created", - "cmd:cmd:field:BucketMetadata.EncryptionConfigUpdatedAt", - "cmd:cmd:field:BucketMetadata.EncryptionConfigXML", - "cmd:cmd:field:BucketMetadata.LifecycleConfigUpdatedAt", - "cmd:cmd:field:BucketMetadata.LifecycleConfigXML", - "cmd:cmd:field:BucketMetadata.LockEnabled", - "cmd:cmd:field:BucketMetadata.Name", - "cmd:cmd:field:BucketMetadata.NotificationConfigUpdatedAt", - "cmd:cmd:field:BucketMetadata.NotificationConfigXML", - "cmd:cmd:field:BucketMetadata.ObjectLockConfigUpdatedAt", - "cmd:cmd:field:BucketMetadata.ObjectLockConfigXML", - "cmd:cmd:field:BucketMetadata.PolicyConfigJSON", - "cmd:cmd:field:BucketMetadata.PolicyConfigUpdatedAt", - "cmd:cmd:field:BucketMetadata.QuotaConfigJSON", - "cmd:cmd:field:BucketMetadata.QuotaConfigUpdatedAt", - "cmd:cmd:field:BucketMetadata.ReplicationConfigUpdatedAt", - "cmd:cmd:field:BucketMetadata.ReplicationConfigXML", - "cmd:cmd:field:BucketMetadata.TaggingConfigUpdatedAt", - "cmd:cmd:field:BucketMetadata.TaggingConfigXML", - "cmd:cmd:field:BucketMetadata.VersioningConfigUpdatedAt", - "cmd:cmd:field:BucketMetadata.VersioningConfigXML", - "cmd:cmd:field:BucketOptions.Cached", - "cmd:cmd:field:BucketOptions.Deleted", - "cmd:cmd:field:BucketOptions.NoMetadata", - "cmd:cmd:field:BucketRemoteIdenticalToSource.Endpoint", - "cmd:cmd:field:BucketReplicationResyncStatus.ID", - "cmd:cmd:field:BucketReplicationResyncStatus.LastUpdate", - "cmd:cmd:field:BucketReplicationResyncStatus.TargetsMap", - "cmd:cmd:field:BucketReplicationResyncStatus.Version", - "cmd:cmd:field:BucketReplicationStat.BandWidthLimitInBytesPerSecond", - "cmd:cmd:field:BucketReplicationStat.CurrentBandwidthInBytesPerSecond", - "cmd:cmd:field:BucketReplicationStat.FailStats", - "cmd:cmd:field:BucketReplicationStat.Failed", - "cmd:cmd:field:BucketReplicationStat.FailedCount", - "cmd:cmd:field:BucketReplicationStat.FailedSize", - "cmd:cmd:field:BucketReplicationStat.Latency", - "cmd:cmd:field:BucketReplicationStat.PendingCount", - "cmd:cmd:field:BucketReplicationStat.PendingSize", - "cmd:cmd:field:BucketReplicationStat.ReplicaSize", - "cmd:cmd:field:BucketReplicationStat.ReplicatedCount", - "cmd:cmd:field:BucketReplicationStat.ReplicatedSize", - "cmd:cmd:field:BucketReplicationStat.XferRateLrg", - "cmd:cmd:field:BucketReplicationStat.XferRateSml", - "cmd:cmd:field:BucketReplicationStats.Failed", - "cmd:cmd:field:BucketReplicationStats.FailedCount", - "cmd:cmd:field:BucketReplicationStats.FailedSize", - "cmd:cmd:field:BucketReplicationStats.PendingCount", - "cmd:cmd:field:BucketReplicationStats.PendingSize", - "cmd:cmd:field:BucketReplicationStats.QStat", - "cmd:cmd:field:BucketReplicationStats.ReplicaCount", - "cmd:cmd:field:BucketReplicationStats.ReplicaSize", - "cmd:cmd:field:BucketReplicationStats.ReplicatedCount", - "cmd:cmd:field:BucketReplicationStats.ReplicatedSize", - "cmd:cmd:field:BucketReplicationStats.Stats", - "cmd:cmd:field:BucketStats.ProxyStats", - "cmd:cmd:field:BucketStats.QueueStats", - "cmd:cmd:field:BucketStats.ReplicationStats", - "cmd:cmd:field:BucketStats.Uptime", - "cmd:cmd:field:BucketStatsMap.Stats", - "cmd:cmd:field:BucketStatsMap.Timestamp", - "cmd:cmd:field:BucketTargetUsageInfo.ReplicaSize", - "cmd:cmd:field:BucketTargetUsageInfo.ReplicatedCount", - "cmd:cmd:field:BucketTargetUsageInfo.ReplicatedSize", - "cmd:cmd:field:BucketTargetUsageInfo.ReplicationFailedCount", - "cmd:cmd:field:BucketTargetUsageInfo.ReplicationFailedSize", - "cmd:cmd:field:BucketTargetUsageInfo.ReplicationPendingCount", - "cmd:cmd:field:BucketTargetUsageInfo.ReplicationPendingSize", - "cmd:cmd:field:BucketUsageInfo.DeleteMarkersCount", - "cmd:cmd:field:BucketUsageInfo.ObjectSizesHistogram", - "cmd:cmd:field:BucketUsageInfo.ObjectVersionsHistogram", - "cmd:cmd:field:BucketUsageInfo.ObjectsCount", - "cmd:cmd:field:BucketUsageInfo.ReplicaCount", - "cmd:cmd:field:BucketUsageInfo.ReplicaSize", - "cmd:cmd:field:BucketUsageInfo.ReplicatedSizeV1", - "cmd:cmd:field:BucketUsageInfo.ReplicationFailedCountV1", - "cmd:cmd:field:BucketUsageInfo.ReplicationFailedSizeV1", - "cmd:cmd:field:BucketUsageInfo.ReplicationInfo", - "cmd:cmd:field:BucketUsageInfo.ReplicationPendingCountV1", - "cmd:cmd:field:BucketUsageInfo.ReplicationPendingSizeV1", - "cmd:cmd:field:BucketUsageInfo.Size", - "cmd:cmd:field:BucketUsageInfo.VersionsCount", - "cmd:cmd:field:CheckPartsHandlerParams.DiskID", - "cmd:cmd:field:CheckPartsHandlerParams.FI", - "cmd:cmd:field:CheckPartsHandlerParams.FilePath", - "cmd:cmd:field:CheckPartsHandlerParams.Volume", - "cmd:cmd:field:CheckPartsResp.Results", - "cmd:cmd:field:ChecksumInfo.Algorithm", - "cmd:cmd:field:ChecksumInfo.Hash", - "cmd:cmd:field:ChecksumInfo.PartNumber", - "cmd:cmd:field:ClientGrantsResult.AssumedRoleUser", - "cmd:cmd:field:ClientGrantsResult.Audience", - "cmd:cmd:field:ClientGrantsResult.Credentials", - "cmd:cmd:field:ClientGrantsResult.PackedPolicySize", - "cmd:cmd:field:ClientGrantsResult.Provider", - "cmd:cmd:field:ClientGrantsResult.SubjectFromToken", - "cmd:cmd:field:CommonPrefix.Prefix", - "cmd:cmd:field:CompleteMultipartUpload.Parts", - "cmd:cmd:field:CompleteMultipartUploadResponse.Bucket", - "cmd:cmd:field:CompleteMultipartUploadResponse.ChecksumCRC32", - "cmd:cmd:field:CompleteMultipartUploadResponse.ChecksumCRC32C", - "cmd:cmd:field:CompleteMultipartUploadResponse.ChecksumCRC64NVME", - "cmd:cmd:field:CompleteMultipartUploadResponse.ChecksumSHA1", - "cmd:cmd:field:CompleteMultipartUploadResponse.ChecksumSHA256", - "cmd:cmd:field:CompleteMultipartUploadResponse.ETag", - "cmd:cmd:field:CompleteMultipartUploadResponse.Key", - "cmd:cmd:field:CompleteMultipartUploadResponse.Location", - "cmd:cmd:field:CompleteMultipartUploadResponse.XMLName", - "cmd:cmd:field:CompletePart.ChecksumCRC32", - "cmd:cmd:field:CompletePart.ChecksumCRC32C", - "cmd:cmd:field:CompletePart.ChecksumCRC64NVME", - "cmd:cmd:field:CompletePart.ChecksumSHA1", - "cmd:cmd:field:CompletePart.ChecksumSHA256", - "cmd:cmd:field:CompletePart.ETag", - "cmd:cmd:field:CompletePart.PartNumber", - "cmd:cmd:field:CompletePart.Size", - "cmd:cmd:field:ConsoleLogger.Enable", - "cmd:cmd:field:CopyObjectPartResponse.ETag", - "cmd:cmd:field:CopyObjectPartResponse.LastModified", - "cmd:cmd:field:CopyObjectPartResponse.XMLName", - "cmd:cmd:field:CopyObjectResponse.ETag", - "cmd:cmd:field:CopyObjectResponse.LastModified", - "cmd:cmd:field:CopyObjectResponse.XMLName", - "cmd:cmd:field:DataUsageInfo.BucketSizes", - "cmd:cmd:field:DataUsageInfo.BucketsCount", - "cmd:cmd:field:DataUsageInfo.BucketsUsage", - "cmd:cmd:field:DataUsageInfo.DeleteMarkersTotalCount", - "cmd:cmd:field:DataUsageInfo.LastUpdate", - "cmd:cmd:field:DataUsageInfo.ObjectsTotalCount", - "cmd:cmd:field:DataUsageInfo.ObjectsTotalSize", - "cmd:cmd:field:DataUsageInfo.ReplicationInfo", - "cmd:cmd:field:DataUsageInfo.TierStats", - "cmd:cmd:field:DataUsageInfo.TotalCapacity", - "cmd:cmd:field:DataUsageInfo.TotalFreeCapacity", - "cmd:cmd:field:DataUsageInfo.TotalUsedCapacity", - "cmd:cmd:field:DataUsageInfo.VersionsTotalCount", - "cmd:cmd:field:DeleteBucketOptions.Force", - "cmd:cmd:field:DeleteBucketOptions.NoLock", - "cmd:cmd:field:DeleteBucketOptions.NoRecreate", - "cmd:cmd:field:DeleteBucketOptions.SRDeleteOp", - "cmd:cmd:field:DeleteBulkReq.Paths", - "cmd:cmd:field:DeleteError.Code", - "cmd:cmd:field:DeleteError.Key", - "cmd:cmd:field:DeleteError.Message", - "cmd:cmd:field:DeleteError.VersionID", - "cmd:cmd:field:DeleteFileHandlerParams.DiskID", - "cmd:cmd:field:DeleteFileHandlerParams.FilePath", - "cmd:cmd:field:DeleteFileHandlerParams.Opts", - "cmd:cmd:field:DeleteFileHandlerParams.Volume", - "cmd:cmd:field:DeleteMarkerVersion.IsLatest", - "cmd:cmd:field:DeleteMarkerVersion.Key", - "cmd:cmd:field:DeleteMarkerVersion.LastModified", - "cmd:cmd:field:DeleteMarkerVersion.Owner", - "cmd:cmd:field:DeleteMarkerVersion.VersionID", - "cmd:cmd:field:DeleteObjectsRequest.Objects", - "cmd:cmd:field:DeleteObjectsRequest.Quiet", - "cmd:cmd:field:DeleteObjectsResponse.DeletedObjects", - "cmd:cmd:field:DeleteObjectsResponse.Errors", - "cmd:cmd:field:DeleteObjectsResponse.XMLName", - "cmd:cmd:field:DeleteOptions.Immediate", - "cmd:cmd:field:DeleteOptions.OldDataDir", - "cmd:cmd:field:DeleteOptions.Recursive", - "cmd:cmd:field:DeleteOptions.UndoWrite", - "cmd:cmd:field:DeleteVersionHandlerParams.DiskID", - "cmd:cmd:field:DeleteVersionHandlerParams.FI", - "cmd:cmd:field:DeleteVersionHandlerParams.FilePath", - "cmd:cmd:field:DeleteVersionHandlerParams.ForceDelMarker", - "cmd:cmd:field:DeleteVersionHandlerParams.Opts", - "cmd:cmd:field:DeleteVersionHandlerParams.Volume", - "cmd:cmd:field:DeleteVersionsErrsResp.Errs", - "cmd:cmd:field:DeletedObject.DeleteMarker", - "cmd:cmd:field:DeletedObject.DeleteMarkerMTime", - "cmd:cmd:field:DeletedObject.DeleteMarkerVersionID", - "cmd:cmd:field:DeletedObject.ObjectName", - "cmd:cmd:field:DeletedObject.ReplicationState", - "cmd:cmd:field:DeletedObject.VersionID", - "cmd:cmd:field:DeletedObjectInfo.Bucket", - "cmd:cmd:field:DeletedObjectInfo.IsLatest", - "cmd:cmd:field:DeletedObjectInfo.ModTime", - "cmd:cmd:field:DeletedObjectInfo.Name", - "cmd:cmd:field:DeletedObjectInfo.VersionID", - "cmd:cmd:field:DeletedObjectReplicationInfo.Bucket", - "cmd:cmd:field:DeletedObjectReplicationInfo.EventType", - "cmd:cmd:field:DeletedObjectReplicationInfo.OpType", - "cmd:cmd:field:DeletedObjectReplicationInfo.ResetID", - "cmd:cmd:field:DeletedObjectReplicationInfo.TargetArn", - "cmd:cmd:field:DiskInfo.Endpoint", - "cmd:cmd:field:DiskInfo.Error", - "cmd:cmd:field:DiskInfo.FSType", - "cmd:cmd:field:DiskInfo.Free", - "cmd:cmd:field:DiskInfo.FreeInodes", - "cmd:cmd:field:DiskInfo.Healing", - "cmd:cmd:field:DiskInfo.ID", - "cmd:cmd:field:DiskInfo.Major", - "cmd:cmd:field:DiskInfo.Metrics", - "cmd:cmd:field:DiskInfo.Minor", - "cmd:cmd:field:DiskInfo.MountPath", - "cmd:cmd:field:DiskInfo.NRRequests", - "cmd:cmd:field:DiskInfo.RootDisk", - "cmd:cmd:field:DiskInfo.Rotational", - "cmd:cmd:field:DiskInfo.Scanning", - "cmd:cmd:field:DiskInfo.Total", - "cmd:cmd:field:DiskInfo.Used", - "cmd:cmd:field:DiskInfo.UsedInodes", - "cmd:cmd:field:DiskInfoOptions.DiskID", - "cmd:cmd:field:DiskInfoOptions.Metrics", - "cmd:cmd:field:DiskInfoOptions.NoOp", - "cmd:cmd:field:DiskMetrics.APICalls", - "cmd:cmd:field:DiskMetrics.LastMinute", - "cmd:cmd:field:DiskMetrics.TotalDeletes", - "cmd:cmd:field:DiskMetrics.TotalErrorsAvailability", - "cmd:cmd:field:DiskMetrics.TotalErrorsTimeout", - "cmd:cmd:field:DiskMetrics.TotalWaiting", - "cmd:cmd:field:DiskMetrics.TotalWrites", - "cmd:cmd:field:Encryption.EncryptionType", - "cmd:cmd:field:Encryption.KMSContext", - "cmd:cmd:field:Encryption.KMSKeyID", - "cmd:cmd:field:Endpoint.DiskIdx", - "cmd:cmd:field:Endpoint.IsLocal", - "cmd:cmd:field:Endpoint.PoolIdx", - "cmd:cmd:field:Endpoint.SetIdx", - "cmd:cmd:field:ErasureInfo.Algorithm", - "cmd:cmd:field:ErasureInfo.BlockSize", - "cmd:cmd:field:ErasureInfo.Checksums", - "cmd:cmd:field:ErasureInfo.DataBlocks", - "cmd:cmd:field:ErasureInfo.Distribution", - "cmd:cmd:field:ErasureInfo.Index", - "cmd:cmd:field:ErasureInfo.ParityBlocks", - "cmd:cmd:field:ExpirationOptions.Expire", - "cmd:cmd:field:FileInfo.Checksum", - "cmd:cmd:field:FileInfo.Data", - "cmd:cmd:field:FileInfo.DataDir", - "cmd:cmd:field:FileInfo.Deleted", - "cmd:cmd:field:FileInfo.Erasure", - "cmd:cmd:field:FileInfo.ExpireRestored", - "cmd:cmd:field:FileInfo.Fresh", - "cmd:cmd:field:FileInfo.Idx", - "cmd:cmd:field:FileInfo.IsLatest", - "cmd:cmd:field:FileInfo.MarkDeleted", - "cmd:cmd:field:FileInfo.Metadata", - "cmd:cmd:field:FileInfo.ModTime", - "cmd:cmd:field:FileInfo.Mode", - "cmd:cmd:field:FileInfo.Name", - "cmd:cmd:field:FileInfo.NumVersions", - "cmd:cmd:field:FileInfo.Parts", - "cmd:cmd:field:FileInfo.ReplicationState", - "cmd:cmd:field:FileInfo.Size", - "cmd:cmd:field:FileInfo.SuccessorModTime", - "cmd:cmd:field:FileInfo.TransitionStatus", - "cmd:cmd:field:FileInfo.TransitionTier", - "cmd:cmd:field:FileInfo.TransitionVersionID", - "cmd:cmd:field:FileInfo.TransitionedObjName", - "cmd:cmd:field:FileInfo.VersionID", - "cmd:cmd:field:FileInfo.Versioned", - "cmd:cmd:field:FileInfo.Volume", - "cmd:cmd:field:FileInfo.WrittenByVersion", - "cmd:cmd:field:FileInfo.XLV1", - "cmd:cmd:field:FileInfoVersions.FreeVersions", - "cmd:cmd:field:FileInfoVersions.LatestModTime", - "cmd:cmd:field:FileInfoVersions.Name", - "cmd:cmd:field:FileInfoVersions.Versions", - "cmd:cmd:field:FileInfoVersions.Volume", - "cmd:cmd:field:FileLogger.Enable", - "cmd:cmd:field:FileLogger.Filename", - "cmd:cmd:field:FilesInfo.Files", - "cmd:cmd:field:FilesInfo.IsTruncated", - "cmd:cmd:field:GenericError.Bucket", - "cmd:cmd:field:GenericError.Err", - "cmd:cmd:field:GenericError.Object", - "cmd:cmd:field:GenericError.VersionID", - "cmd:cmd:field:GetObjectReader.ObjInfo", - "cmd:cmd:field:GroupInfo.Members", - "cmd:cmd:field:GroupInfo.Status", - "cmd:cmd:field:GroupInfo.UpdatedAt", - "cmd:cmd:field:GroupInfo.Version", - "cmd:cmd:field:HTTPRangeSpec.End", - "cmd:cmd:field:HTTPRangeSpec.IsSuffixLength", - "cmd:cmd:field:HTTPRangeSpec.Start", - "cmd:cmd:field:HealthOptions.DeploymentType", - "cmd:cmd:field:HealthOptions.Maintenance", - "cmd:cmd:field:HealthOptions.NoLogging", - "cmd:cmd:field:HealthResult.ESHealth", - "cmd:cmd:field:HealthResult.HealingDrives", - "cmd:cmd:field:HealthResult.Healthy", - "cmd:cmd:field:HealthResult.HealthyRead", - "cmd:cmd:field:HealthResult.ReadQuorum", - "cmd:cmd:field:HealthResult.UsingDefaults", - "cmd:cmd:field:HealthResult.WriteQuorum", - "cmd:cmd:field:Help.Description", - "cmd:cmd:field:Help.KeysHelp", - "cmd:cmd:field:Help.MultipleTargets", - "cmd:cmd:field:Help.SubSys", - "cmd:cmd:field:IAMSys.LDAPConfig", - "cmd:cmd:field:IAMSys.LastRefreshDurationMilliseconds", - "cmd:cmd:field:IAMSys.LastRefreshTimeUnixNano", - "cmd:cmd:field:IAMSys.OpenIDConfig", - "cmd:cmd:field:IAMSys.STSTLSConfig", - "cmd:cmd:field:IAMSys.TotalRefreshFailures", - "cmd:cmd:field:IAMSys.TotalRefreshSuccesses", - "cmd:cmd:field:InQueueMetric.Avg", - "cmd:cmd:field:InQueueMetric.Curr", - "cmd:cmd:field:InQueueMetric.Max", - "cmd:cmd:field:InitiateMultipartUploadResponse.Bucket", - "cmd:cmd:field:InitiateMultipartUploadResponse.Key", - "cmd:cmd:field:InitiateMultipartUploadResponse.UploadID", - "cmd:cmd:field:InitiateMultipartUploadResponse.XMLName", - "cmd:cmd:field:InsufficientReadQuorum.Bucket", - "cmd:cmd:field:InsufficientReadQuorum.Err", - "cmd:cmd:field:InsufficientReadQuorum.Object", - "cmd:cmd:field:InsufficientReadQuorum.Type", - "cmd:cmd:field:InvalidPart.ExpETag", - "cmd:cmd:field:InvalidPart.GotETag", - "cmd:cmd:field:InvalidPart.PartNumber", - "cmd:cmd:field:InvalidRange.OffsetBegin", - "cmd:cmd:field:InvalidRange.OffsetEnd", - "cmd:cmd:field:InvalidRange.ResourceSize", - "cmd:cmd:field:InvalidUploadID.Bucket", - "cmd:cmd:field:InvalidUploadID.Object", - "cmd:cmd:field:InvalidUploadID.UploadID", - "cmd:cmd:field:InvalidUploadIDKeyCombination.KeyMarker", - "cmd:cmd:field:InvalidUploadIDKeyCombination.UploadIDMarker", - "cmd:cmd:field:LDAPIdentityResult.Credentials", - "cmd:cmd:field:ListBucketsResponse.Buckets", - "cmd:cmd:field:ListBucketsResponse.Owner", - "cmd:cmd:field:ListBucketsResponse.XMLName", - "cmd:cmd:field:ListDirResult.Entries", - "cmd:cmd:field:ListMultipartUploadsResponse.Bucket", - "cmd:cmd:field:ListMultipartUploadsResponse.CommonPrefixes", - "cmd:cmd:field:ListMultipartUploadsResponse.Delimiter", - "cmd:cmd:field:ListMultipartUploadsResponse.EncodingType", - "cmd:cmd:field:ListMultipartUploadsResponse.IsTruncated", - "cmd:cmd:field:ListMultipartUploadsResponse.KeyMarker", - "cmd:cmd:field:ListMultipartUploadsResponse.MaxUploads", - "cmd:cmd:field:ListMultipartUploadsResponse.NextKeyMarker", - "cmd:cmd:field:ListMultipartUploadsResponse.NextUploadIDMarker", - "cmd:cmd:field:ListMultipartUploadsResponse.Prefix", - "cmd:cmd:field:ListMultipartUploadsResponse.UploadIDMarker", - "cmd:cmd:field:ListMultipartUploadsResponse.Uploads", - "cmd:cmd:field:ListMultipartUploadsResponse.XMLName", - "cmd:cmd:field:ListMultipartsInfo.CommonPrefixes", - "cmd:cmd:field:ListMultipartsInfo.Delimiter", - "cmd:cmd:field:ListMultipartsInfo.EncodingType", - "cmd:cmd:field:ListMultipartsInfo.IsTruncated", - "cmd:cmd:field:ListMultipartsInfo.KeyMarker", - "cmd:cmd:field:ListMultipartsInfo.MaxUploads", - "cmd:cmd:field:ListMultipartsInfo.NextKeyMarker", - "cmd:cmd:field:ListMultipartsInfo.NextUploadIDMarker", - "cmd:cmd:field:ListMultipartsInfo.Prefix", - "cmd:cmd:field:ListMultipartsInfo.UploadIDMarker", - "cmd:cmd:field:ListMultipartsInfo.Uploads", - "cmd:cmd:field:ListObjectVersionsInfo.IsTruncated", - "cmd:cmd:field:ListObjectVersionsInfo.NextMarker", - "cmd:cmd:field:ListObjectVersionsInfo.NextVersionIDMarker", - "cmd:cmd:field:ListObjectVersionsInfo.Objects", - "cmd:cmd:field:ListObjectVersionsInfo.Prefixes", - "cmd:cmd:field:ListObjectsInfo.IsTruncated", - "cmd:cmd:field:ListObjectsInfo.NextMarker", - "cmd:cmd:field:ListObjectsInfo.Objects", - "cmd:cmd:field:ListObjectsInfo.Prefixes", - "cmd:cmd:field:ListObjectsResponse.CommonPrefixes", - "cmd:cmd:field:ListObjectsResponse.Contents", - "cmd:cmd:field:ListObjectsResponse.Delimiter", - "cmd:cmd:field:ListObjectsResponse.EncodingType", - "cmd:cmd:field:ListObjectsResponse.IsTruncated", - "cmd:cmd:field:ListObjectsResponse.Marker", - "cmd:cmd:field:ListObjectsResponse.MaxKeys", - "cmd:cmd:field:ListObjectsResponse.Name", - "cmd:cmd:field:ListObjectsResponse.NextMarker", - "cmd:cmd:field:ListObjectsResponse.Prefix", - "cmd:cmd:field:ListObjectsResponse.XMLName", - "cmd:cmd:field:ListObjectsV2Info.ContinuationToken", - "cmd:cmd:field:ListObjectsV2Info.IsTruncated", - "cmd:cmd:field:ListObjectsV2Info.NextContinuationToken", - "cmd:cmd:field:ListObjectsV2Info.Objects", - "cmd:cmd:field:ListObjectsV2Info.Prefixes", - "cmd:cmd:field:ListObjectsV2Response.CommonPrefixes", - "cmd:cmd:field:ListObjectsV2Response.Contents", - "cmd:cmd:field:ListObjectsV2Response.ContinuationToken", - "cmd:cmd:field:ListObjectsV2Response.Delimiter", - "cmd:cmd:field:ListObjectsV2Response.EncodingType", - "cmd:cmd:field:ListObjectsV2Response.IsTruncated", - "cmd:cmd:field:ListObjectsV2Response.KeyCount", - "cmd:cmd:field:ListObjectsV2Response.MaxKeys", - "cmd:cmd:field:ListObjectsV2Response.Name", - "cmd:cmd:field:ListObjectsV2Response.NextContinuationToken", - "cmd:cmd:field:ListObjectsV2Response.Prefix", - "cmd:cmd:field:ListObjectsV2Response.StartAfter", - "cmd:cmd:field:ListObjectsV2Response.XMLName", - "cmd:cmd:field:ListPartsInfo.Bucket", - "cmd:cmd:field:ListPartsInfo.ChecksumAlgorithm", - "cmd:cmd:field:ListPartsInfo.ChecksumType", - "cmd:cmd:field:ListPartsInfo.IsTruncated", - "cmd:cmd:field:ListPartsInfo.MaxParts", - "cmd:cmd:field:ListPartsInfo.NextPartNumberMarker", - "cmd:cmd:field:ListPartsInfo.Object", - "cmd:cmd:field:ListPartsInfo.PartNumberMarker", - "cmd:cmd:field:ListPartsInfo.Parts", - "cmd:cmd:field:ListPartsInfo.StorageClass", - "cmd:cmd:field:ListPartsInfo.UploadID", - "cmd:cmd:field:ListPartsInfo.UserDefined", - "cmd:cmd:field:ListPartsResponse.Bucket", - "cmd:cmd:field:ListPartsResponse.ChecksumAlgorithm", - "cmd:cmd:field:ListPartsResponse.ChecksumType", - "cmd:cmd:field:ListPartsResponse.Initiator", - "cmd:cmd:field:ListPartsResponse.IsTruncated", - "cmd:cmd:field:ListPartsResponse.Key", - "cmd:cmd:field:ListPartsResponse.MaxParts", - "cmd:cmd:field:ListPartsResponse.NextPartNumberMarker", - "cmd:cmd:field:ListPartsResponse.Owner", - "cmd:cmd:field:ListPartsResponse.PartNumberMarker", - "cmd:cmd:field:ListPartsResponse.Parts", - "cmd:cmd:field:ListPartsResponse.StorageClass", - "cmd:cmd:field:ListPartsResponse.UploadID", - "cmd:cmd:field:ListPartsResponse.XMLName", - "cmd:cmd:field:ListVersionsResponse.CommonPrefixes", - "cmd:cmd:field:ListVersionsResponse.Delimiter", - "cmd:cmd:field:ListVersionsResponse.EncodingType", - "cmd:cmd:field:ListVersionsResponse.IsTruncated", - "cmd:cmd:field:ListVersionsResponse.KeyMarker", - "cmd:cmd:field:ListVersionsResponse.MaxKeys", - "cmd:cmd:field:ListVersionsResponse.Name", - "cmd:cmd:field:ListVersionsResponse.NextKeyMarker", - "cmd:cmd:field:ListVersionsResponse.NextVersionIDMarker", - "cmd:cmd:field:ListVersionsResponse.Prefix", - "cmd:cmd:field:ListVersionsResponse.VersionIDMarker", - "cmd:cmd:field:ListVersionsResponse.Versions", - "cmd:cmd:field:ListVersionsResponse.XMLName", - "cmd:cmd:field:LocalDiskIDs.IDs", - "cmd:cmd:field:LocationResponse.Location", - "cmd:cmd:field:LocationResponse.XMLName", - "cmd:cmd:field:MRFReplicateEntries.Entries", - "cmd:cmd:field:MRFReplicateEntries.Version", - "cmd:cmd:field:MRFReplicateEntry.Bucket", - "cmd:cmd:field:MRFReplicateEntry.Object", - "cmd:cmd:field:MRFReplicateEntry.RetryCount", - "cmd:cmd:field:MakeBucketOptions.CreatedAt", - "cmd:cmd:field:MakeBucketOptions.ForceCreate", - "cmd:cmd:field:MakeBucketOptions.LockEnabled", - "cmd:cmd:field:MakeBucketOptions.NoLock", - "cmd:cmd:field:MakeBucketOptions.VersioningEnabled", - "cmd:cmd:field:MalformedUploadID.UploadID", - "cmd:cmd:field:MappedPolicy.Policies", - "cmd:cmd:field:MappedPolicy.UpdatedAt", - "cmd:cmd:field:MappedPolicy.Version", - "cmd:cmd:field:Metadata.Items", - "cmd:cmd:field:MetadataEntry.Name", - "cmd:cmd:field:MetadataEntry.Value", - "cmd:cmd:field:MetadataHandlerParams.DiskID", - "cmd:cmd:field:MetadataHandlerParams.FI", - "cmd:cmd:field:MetadataHandlerParams.FilePath", - "cmd:cmd:field:MetadataHandlerParams.OrigVolume", - "cmd:cmd:field:MetadataHandlerParams.UpdateOpts", - "cmd:cmd:field:MetadataHandlerParams.Volume", - "cmd:cmd:field:MetricDescription.Help", - "cmd:cmd:field:MetricDescription.Name", - "cmd:cmd:field:MetricDescription.Namespace", - "cmd:cmd:field:MetricDescription.Subsystem", - "cmd:cmd:field:MetricDescription.Type", - "cmd:cmd:field:MetricDescriptor.Help", - "cmd:cmd:field:MetricDescriptor.Name", - "cmd:cmd:field:MetricDescriptor.Type", - "cmd:cmd:field:MetricDescriptor.VariableLabels", - "cmd:cmd:field:MetricV2.Description", - "cmd:cmd:field:MetricV2.Histogram", - "cmd:cmd:field:MetricV2.HistogramBucketLabel", - "cmd:cmd:field:MetricV2.StaticLabels", - "cmd:cmd:field:MetricV2.Value", - "cmd:cmd:field:MetricV2.VariableLabels", - "cmd:cmd:field:MetricsGroup.CollectorPath", - "cmd:cmd:field:MetricsGroup.Descriptors", - "cmd:cmd:field:MetricsGroup.ExtraLabels", - "cmd:cmd:field:MultipartInfo.Bucket", - "cmd:cmd:field:MultipartInfo.Initiated", - "cmd:cmd:field:MultipartInfo.Object", - "cmd:cmd:field:MultipartInfo.UploadID", - "cmd:cmd:field:MultipartInfo.UserDefined", - "cmd:cmd:field:NewMultipartUploadResult.ChecksumAlgo", - "cmd:cmd:field:NewMultipartUploadResult.ChecksumType", - "cmd:cmd:field:NewMultipartUploadResult.UploadID", - "cmd:cmd:field:Node.GridHost", - "cmd:cmd:field:Node.IsLocal", - "cmd:cmd:field:Node.Pools", - "cmd:cmd:field:NotImplemented.Message", - "cmd:cmd:field:NotificationPeerErr.Err", - "cmd:cmd:field:NotificationPeerErr.Host", - "cmd:cmd:field:Object.ETag", - "cmd:cmd:field:Object.Internal", - "cmd:cmd:field:Object.Key", - "cmd:cmd:field:Object.LastModified", - "cmd:cmd:field:Object.Owner", - "cmd:cmd:field:Object.Size", - "cmd:cmd:field:Object.StorageClass", - "cmd:cmd:field:Object.UserMetadata", - "cmd:cmd:field:Object.UserTags", - "cmd:cmd:field:ObjectInfo.AccTime", - "cmd:cmd:field:ObjectInfo.ActualSize", - "cmd:cmd:field:ObjectInfo.Bucket", - "cmd:cmd:field:ObjectInfo.CacheControl", - "cmd:cmd:field:ObjectInfo.Checksum", - "cmd:cmd:field:ObjectInfo.ContentEncoding", - "cmd:cmd:field:ObjectInfo.ContentType", - "cmd:cmd:field:ObjectInfo.DataBlocks", - "cmd:cmd:field:ObjectInfo.DeleteMarker", - "cmd:cmd:field:ObjectInfo.ETag", - "cmd:cmd:field:ObjectInfo.Expires", - "cmd:cmd:field:ObjectInfo.Inlined", - "cmd:cmd:field:ObjectInfo.IsDir", - "cmd:cmd:field:ObjectInfo.IsLatest", - "cmd:cmd:field:ObjectInfo.Legacy", - "cmd:cmd:field:ObjectInfo.ModTime", - "cmd:cmd:field:ObjectInfo.Name", - "cmd:cmd:field:ObjectInfo.NumVersions", - "cmd:cmd:field:ObjectInfo.ParityBlocks", - "cmd:cmd:field:ObjectInfo.Parts", - "cmd:cmd:field:ObjectInfo.PutObjReader", - "cmd:cmd:field:ObjectInfo.Reader", - "cmd:cmd:field:ObjectInfo.ReplicationStatus", - "cmd:cmd:field:ObjectInfo.ReplicationStatusInternal", - "cmd:cmd:field:ObjectInfo.RestoreExpires", - "cmd:cmd:field:ObjectInfo.RestoreOngoing", - "cmd:cmd:field:ObjectInfo.Size", - "cmd:cmd:field:ObjectInfo.StorageClass", - "cmd:cmd:field:ObjectInfo.SuccessorModTime", - "cmd:cmd:field:ObjectInfo.TransitionedObject", - "cmd:cmd:field:ObjectInfo.UserDefined", - "cmd:cmd:field:ObjectInfo.UserTags", - "cmd:cmd:field:ObjectInfo.VersionID", - "cmd:cmd:field:ObjectInfo.VersionPurgeStatus", - "cmd:cmd:field:ObjectInfo.VersionPurgeStatusInternal", - "cmd:cmd:field:ObjectInfo.Writer", - "cmd:cmd:field:ObjectInternalInfo.K", - "cmd:cmd:field:ObjectInternalInfo.M", - "cmd:cmd:field:ObjectLayer.AbortMultipartUpload", - "cmd:cmd:field:ObjectLayer.BackendInfo", - "cmd:cmd:field:ObjectLayer.CheckAbandonedParts", - "cmd:cmd:field:ObjectLayer.CompleteMultipartUpload", - "cmd:cmd:field:ObjectLayer.CopyObject", - "cmd:cmd:field:ObjectLayer.CopyObjectPart", - "cmd:cmd:field:ObjectLayer.DecomTieredObject", - "cmd:cmd:field:ObjectLayer.DeleteBucket", - "cmd:cmd:field:ObjectLayer.DeleteObject", - "cmd:cmd:field:ObjectLayer.DeleteObjectTags", - "cmd:cmd:field:ObjectLayer.DeleteObjects", - "cmd:cmd:field:ObjectLayer.GetBucketInfo", - "cmd:cmd:field:ObjectLayer.GetDisks", - "cmd:cmd:field:ObjectLayer.GetMultipartInfo", - "cmd:cmd:field:ObjectLayer.GetObjectInfo", - "cmd:cmd:field:ObjectLayer.GetObjectNInfo", - "cmd:cmd:field:ObjectLayer.GetObjectTags", - "cmd:cmd:field:ObjectLayer.HealBucket", - "cmd:cmd:field:ObjectLayer.HealFormat", - "cmd:cmd:field:ObjectLayer.HealObject", - "cmd:cmd:field:ObjectLayer.HealObjects", - "cmd:cmd:field:ObjectLayer.Health", - "cmd:cmd:field:ObjectLayer.Legacy", - "cmd:cmd:field:ObjectLayer.ListBuckets", - "cmd:cmd:field:ObjectLayer.ListMultipartUploads", - "cmd:cmd:field:ObjectLayer.ListObjectParts", - "cmd:cmd:field:ObjectLayer.ListObjectVersions", - "cmd:cmd:field:ObjectLayer.ListObjects", - "cmd:cmd:field:ObjectLayer.ListObjectsV2", - "cmd:cmd:field:ObjectLayer.LocalStorageInfo", - "cmd:cmd:field:ObjectLayer.MakeBucket", - "cmd:cmd:field:ObjectLayer.NSScanner", - "cmd:cmd:field:ObjectLayer.NewMultipartUpload", - "cmd:cmd:field:ObjectLayer.NewNSLock", - "cmd:cmd:field:ObjectLayer.PutObject", - "cmd:cmd:field:ObjectLayer.PutObjectMetadata", - "cmd:cmd:field:ObjectLayer.PutObjectPart", - "cmd:cmd:field:ObjectLayer.PutObjectTags", - "cmd:cmd:field:ObjectLayer.RestoreTransitionedObject", - "cmd:cmd:field:ObjectLayer.SetDriveCounts", - "cmd:cmd:field:ObjectLayer.Shutdown", - "cmd:cmd:field:ObjectLayer.StorageInfo", - "cmd:cmd:field:ObjectLayer.TransitionObject", - "cmd:cmd:field:ObjectLayer.Walk", - "cmd:cmd:field:ObjectOptions.CheckDMReplicationReady", - "cmd:cmd:field:ObjectOptions.CheckPrecondFn", - "cmd:cmd:field:ObjectOptions.DataMovement", - "cmd:cmd:field:ObjectOptions.DeleteMarker", - "cmd:cmd:field:ObjectOptions.DeletePrefix", - "cmd:cmd:field:ObjectOptions.DeletePrefixObject", - "cmd:cmd:field:ObjectOptions.DeleteReplication", - "cmd:cmd:field:ObjectOptions.EncryptFn", - "cmd:cmd:field:ObjectOptions.EvalMetadataFn", - "cmd:cmd:field:ObjectOptions.EvalRetentionBypassFn", - "cmd:cmd:field:ObjectOptions.Expiration", - "cmd:cmd:field:ObjectOptions.Expires", - "cmd:cmd:field:ObjectOptions.FastGetObjInfo", - "cmd:cmd:field:ObjectOptions.HasIfMatch", - "cmd:cmd:field:ObjectOptions.InclFreeVersions", - "cmd:cmd:field:ObjectOptions.IndexCB", - "cmd:cmd:field:ObjectOptions.LifecycleAuditEvent", - "cmd:cmd:field:ObjectOptions.MTime", - "cmd:cmd:field:ObjectOptions.MaxParity", - "cmd:cmd:field:ObjectOptions.MaxParts", - "cmd:cmd:field:ObjectOptions.MetadataChg", - "cmd:cmd:field:ObjectOptions.NoAuditLog", - "cmd:cmd:field:ObjectOptions.NoDecryption", - "cmd:cmd:field:ObjectOptions.NoLock", - "cmd:cmd:field:ObjectOptions.ObjectAttributes", - "cmd:cmd:field:ObjectOptions.PartNumber", - "cmd:cmd:field:ObjectOptions.PartNumberMarker", - "cmd:cmd:field:ObjectOptions.PrefixEnabledFn", - "cmd:cmd:field:ObjectOptions.PreserveETag", - "cmd:cmd:field:ObjectOptions.ProxyHeaderSet", - "cmd:cmd:field:ObjectOptions.ProxyRequest", - "cmd:cmd:field:ObjectOptions.ReplicationRequest", - "cmd:cmd:field:ObjectOptions.ReplicationSourceLegalholdTimestamp", - "cmd:cmd:field:ObjectOptions.ReplicationSourceRetentionTimestamp", - "cmd:cmd:field:ObjectOptions.ReplicationSourceTaggingTimestamp", - "cmd:cmd:field:ObjectOptions.ServerSideEncryption", - "cmd:cmd:field:ObjectOptions.SkipDecommissioned", - "cmd:cmd:field:ObjectOptions.SkipFreeVersion", - "cmd:cmd:field:ObjectOptions.SkipRebalancing", - "cmd:cmd:field:ObjectOptions.Speedtest", - "cmd:cmd:field:ObjectOptions.SrcPoolIdx", - "cmd:cmd:field:ObjectOptions.Tagging", - "cmd:cmd:field:ObjectOptions.Transition", - "cmd:cmd:field:ObjectOptions.UserDefined", - "cmd:cmd:field:ObjectOptions.VersionID", - "cmd:cmd:field:ObjectOptions.VersionSuspended", - "cmd:cmd:field:ObjectOptions.Versioned", - "cmd:cmd:field:ObjectOptions.WantChecksum", - "cmd:cmd:field:ObjectOptions.WantServerSideChecksumType", - "cmd:cmd:field:ObjectPartInfo.ActualSize", - "cmd:cmd:field:ObjectPartInfo.Checksums", - "cmd:cmd:field:ObjectPartInfo.ETag", - "cmd:cmd:field:ObjectPartInfo.Error", - "cmd:cmd:field:ObjectPartInfo.Index", - "cmd:cmd:field:ObjectPartInfo.ModTime", - "cmd:cmd:field:ObjectPartInfo.Number", - "cmd:cmd:field:ObjectPartInfo.Size", - "cmd:cmd:field:ObjectTagSet.Tags", - "cmd:cmd:field:ObjectToDelete.DeleteMarkerReplicationStatus", - "cmd:cmd:field:ObjectToDelete.ReplicateDecisionStr", - "cmd:cmd:field:ObjectToDelete.VersionPurgeStatus", - "cmd:cmd:field:ObjectToDelete.VersionPurgeStatuses", - "cmd:cmd:field:ObjectV.ObjectName", - "cmd:cmd:field:ObjectV.VersionID", - "cmd:cmd:field:ObjectVersion.IsLatest", - "cmd:cmd:field:ObjectVersion.VersionID", - "cmd:cmd:field:OpenIDClientAppParams.ClientID", - "cmd:cmd:field:OpenIDClientAppParams.ClientSecret", - "cmd:cmd:field:OpenIDClientAppParams.ProviderURL", - "cmd:cmd:field:OpenIDClientAppParams.RedirectURL", - "cmd:cmd:field:OutputLocation.S3", - "cmd:cmd:field:Owner.DisplayName", - "cmd:cmd:field:Owner.ID", - "cmd:cmd:field:Part.ChecksumCRC32", - "cmd:cmd:field:Part.ChecksumCRC32C", - "cmd:cmd:field:Part.ChecksumCRC64NVME", - "cmd:cmd:field:Part.ChecksumSHA1", - "cmd:cmd:field:Part.ChecksumSHA256", - "cmd:cmd:field:Part.ETag", - "cmd:cmd:field:Part.LastModified", - "cmd:cmd:field:Part.PartNumber", - "cmd:cmd:field:Part.Size", - "cmd:cmd:field:PartInfo.ActualSize", - "cmd:cmd:field:PartInfo.ChecksumCRC32", - "cmd:cmd:field:PartInfo.ChecksumCRC32C", - "cmd:cmd:field:PartInfo.ChecksumCRC64NVME", - "cmd:cmd:field:PartInfo.ChecksumSHA1", - "cmd:cmd:field:PartInfo.ChecksumSHA256", - "cmd:cmd:field:PartInfo.ETag", - "cmd:cmd:field:PartInfo.LastModified", - "cmd:cmd:field:PartInfo.PartNumber", - "cmd:cmd:field:PartInfo.Size", - "cmd:cmd:field:PartTooSmall.PartETag", - "cmd:cmd:field:PartTooSmall.PartNumber", - "cmd:cmd:field:PartTooSmall.PartSize", - "cmd:cmd:field:PartialOperation.BitrotScan", - "cmd:cmd:field:PartialOperation.Bucket", - "cmd:cmd:field:PartialOperation.Object", - "cmd:cmd:field:PartialOperation.PoolIndex", - "cmd:cmd:field:PartialOperation.Queued", - "cmd:cmd:field:PartialOperation.SetIndex", - "cmd:cmd:field:PartialOperation.VersionID", - "cmd:cmd:field:PartialOperation.Versions", - "cmd:cmd:field:PeerLocks.Addr", - "cmd:cmd:field:PeerLocks.Locks", - "cmd:cmd:field:PeerResourceMetrics.Errors", - "cmd:cmd:field:PeerResourceMetrics.Metrics", - "cmd:cmd:field:PeerSiteInfo.DeploymentID", - "cmd:cmd:field:PeerSiteInfo.Empty", - "cmd:cmd:field:PeerSiteInfo.Replicated", - "cmd:cmd:field:PolicyDoc.CreateDate", - "cmd:cmd:field:PolicyDoc.Policy", - "cmd:cmd:field:PolicyDoc.UpdateDate", - "cmd:cmd:field:PolicyDoc.Version", - "cmd:cmd:field:PolicyStatus.IsPublic", - "cmd:cmd:field:PolicyStatus.XMLName", - "cmd:cmd:field:PoolDecommissionInfo.Bucket", - "cmd:cmd:field:PoolDecommissionInfo.BytesDone", - "cmd:cmd:field:PoolDecommissionInfo.BytesFailed", - "cmd:cmd:field:PoolDecommissionInfo.Canceled", - "cmd:cmd:field:PoolDecommissionInfo.Complete", - "cmd:cmd:field:PoolDecommissionInfo.CurrentSize", - "cmd:cmd:field:PoolDecommissionInfo.DecommissionedBuckets", - "cmd:cmd:field:PoolDecommissionInfo.Failed", - "cmd:cmd:field:PoolDecommissionInfo.ItemsDecommissionFailed", - "cmd:cmd:field:PoolDecommissionInfo.ItemsDecommissioned", - "cmd:cmd:field:PoolDecommissionInfo.Object", - "cmd:cmd:field:PoolDecommissionInfo.Prefix", - "cmd:cmd:field:PoolDecommissionInfo.QueuedBuckets", - "cmd:cmd:field:PoolDecommissionInfo.StartSize", - "cmd:cmd:field:PoolDecommissionInfo.StartTime", - "cmd:cmd:field:PoolDecommissionInfo.TotalSize", - "cmd:cmd:field:PoolEndpoints.CmdLine", - "cmd:cmd:field:PoolEndpoints.DrivesPerSet", - "cmd:cmd:field:PoolEndpoints.Endpoints", - "cmd:cmd:field:PoolEndpoints.Legacy", - "cmd:cmd:field:PoolEndpoints.Platform", - "cmd:cmd:field:PoolEndpoints.SetCount", - "cmd:cmd:field:PoolObjInfo.Err", - "cmd:cmd:field:PoolObjInfo.Index", - "cmd:cmd:field:PoolObjInfo.ObjInfo", - "cmd:cmd:field:PoolStatus.CmdLine", - "cmd:cmd:field:PoolStatus.Decommission", - "cmd:cmd:field:PoolStatus.ID", - "cmd:cmd:field:PoolStatus.LastUpdate", - "cmd:cmd:field:PostPolicyForm.Conditions", - "cmd:cmd:field:PostPolicyForm.Expiration", - "cmd:cmd:field:PostResponse.Bucket", - "cmd:cmd:field:PostResponse.ETag", - "cmd:cmd:field:PostResponse.Key", - "cmd:cmd:field:PostResponse.Location", - "cmd:cmd:field:ProxyEndpoint.Transport", - "cmd:cmd:field:ProxyMetric.GetFailedTotal", - "cmd:cmd:field:ProxyMetric.GetTagFailedTotal", - "cmd:cmd:field:ProxyMetric.GetTagTotal", - "cmd:cmd:field:ProxyMetric.GetTotal", - "cmd:cmd:field:ProxyMetric.HeadFailedTotal", - "cmd:cmd:field:ProxyMetric.HeadTotal", - "cmd:cmd:field:ProxyMetric.PutTagFailedTotal", - "cmd:cmd:field:ProxyMetric.PutTagTotal", - "cmd:cmd:field:ProxyMetric.RmvTagFailedTotal", - "cmd:cmd:field:ProxyMetric.RmvTagTotal", - "cmd:cmd:field:QStat.Bytes", - "cmd:cmd:field:QStat.Count", - "cmd:cmd:field:RStat.Bytes", - "cmd:cmd:field:RStat.Count", - "cmd:cmd:field:RTimedMetrics.ErrCounts", - "cmd:cmd:field:RTimedMetrics.LastHour", - "cmd:cmd:field:RTimedMetrics.LastMinute", - "cmd:cmd:field:RTimedMetrics.SinceUptime", - "cmd:cmd:field:RWLocker.GetLock", - "cmd:cmd:field:RWLocker.GetRLock", - "cmd:cmd:field:RWLocker.RUnlock", - "cmd:cmd:field:RWLocker.Unlock", - "cmd:cmd:field:RawFileInfo.Buf", - "cmd:cmd:field:ReadAllHandlerParams.DiskID", - "cmd:cmd:field:ReadAllHandlerParams.FilePath", - "cmd:cmd:field:ReadAllHandlerParams.Volume", - "cmd:cmd:field:ReadOptions.Healing", - "cmd:cmd:field:ReadOptions.InclFreeVersions", - "cmd:cmd:field:ReadOptions.ReadData", - "cmd:cmd:field:ReadPartsReq.Paths", - "cmd:cmd:field:ReadPartsResp.Infos", - "cmd:cmd:field:RemoteTargetConnectionErr.AccessKey", - "cmd:cmd:field:RemoteTargetConnectionErr.Bucket", - "cmd:cmd:field:RemoteTargetConnectionErr.Endpoint", - "cmd:cmd:field:RemoteTargetConnectionErr.Err", - "cmd:cmd:field:RenameDataHandlerParams.DiskID", - "cmd:cmd:field:RenameDataHandlerParams.DstPath", - "cmd:cmd:field:RenameDataHandlerParams.DstVolume", - "cmd:cmd:field:RenameDataHandlerParams.FI", - "cmd:cmd:field:RenameDataHandlerParams.Opts", - "cmd:cmd:field:RenameDataHandlerParams.SrcPath", - "cmd:cmd:field:RenameDataHandlerParams.SrcVolume", - "cmd:cmd:field:RenameDataResp.OldDataDir", - "cmd:cmd:field:RenameDataResp.Sign", - "cmd:cmd:field:RenameFileHandlerParams.DiskID", - "cmd:cmd:field:RenameFileHandlerParams.DstFilePath", - "cmd:cmd:field:RenameFileHandlerParams.DstVolume", - "cmd:cmd:field:RenameFileHandlerParams.SrcFilePath", - "cmd:cmd:field:RenameFileHandlerParams.SrcVolume", - "cmd:cmd:field:RenamePartHandlerParams.DiskID", - "cmd:cmd:field:RenamePartHandlerParams.DstFilePath", - "cmd:cmd:field:RenamePartHandlerParams.DstVolume", - "cmd:cmd:field:RenamePartHandlerParams.Meta", - "cmd:cmd:field:RenamePartHandlerParams.SkipParent", - "cmd:cmd:field:RenamePartHandlerParams.SrcFilePath", - "cmd:cmd:field:RenamePartHandlerParams.SrcVolume", - "cmd:cmd:field:ReplQNodeStats.ActiveWorkers", - "cmd:cmd:field:ReplQNodeStats.MRFStats", - "cmd:cmd:field:ReplQNodeStats.NodeName", - "cmd:cmd:field:ReplQNodeStats.QStats", - "cmd:cmd:field:ReplQNodeStats.TgtXferStats", - "cmd:cmd:field:ReplQNodeStats.Uptime", - "cmd:cmd:field:ReplQNodeStats.XferStats", - "cmd:cmd:field:ReplicateObjectInfo.ActualSize", - "cmd:cmd:field:ReplicateObjectInfo.Bucket", - "cmd:cmd:field:ReplicateObjectInfo.Checksum", - "cmd:cmd:field:ReplicateObjectInfo.DeleteMarker", - "cmd:cmd:field:ReplicateObjectInfo.Dsc", - "cmd:cmd:field:ReplicateObjectInfo.ETag", - "cmd:cmd:field:ReplicateObjectInfo.EventType", - "cmd:cmd:field:ReplicateObjectInfo.ExistingObjResync", - "cmd:cmd:field:ReplicateObjectInfo.ModTime", - "cmd:cmd:field:ReplicateObjectInfo.Name", - "cmd:cmd:field:ReplicateObjectInfo.OpType", - "cmd:cmd:field:ReplicateObjectInfo.ReplicationState", - "cmd:cmd:field:ReplicateObjectInfo.ReplicationStatus", - "cmd:cmd:field:ReplicateObjectInfo.ReplicationStatusInternal", - "cmd:cmd:field:ReplicateObjectInfo.ReplicationTimestamp", - "cmd:cmd:field:ReplicateObjectInfo.ResetID", - "cmd:cmd:field:ReplicateObjectInfo.RetryCount", - "cmd:cmd:field:ReplicateObjectInfo.SSEC", - "cmd:cmd:field:ReplicateObjectInfo.Size", - "cmd:cmd:field:ReplicateObjectInfo.TargetArn", - "cmd:cmd:field:ReplicateObjectInfo.TargetPurgeStatuses", - "cmd:cmd:field:ReplicateObjectInfo.TargetStatuses", - "cmd:cmd:field:ReplicateObjectInfo.UserTags", - "cmd:cmd:field:ReplicateObjectInfo.VersionID", - "cmd:cmd:field:ReplicateObjectInfo.VersionPurgeStatus", - "cmd:cmd:field:ReplicateObjectInfo.VersionPurgeStatusInternal", - "cmd:cmd:field:ReplicationLastHour.LastMin", - "cmd:cmd:field:ReplicationLastHour.Totals", - "cmd:cmd:field:ReplicationLastMinute.LastMinute", - "cmd:cmd:field:ReplicationLatency.UploadHistogram", - "cmd:cmd:field:ReplicationMRFStats.LastFailedCount", - "cmd:cmd:field:ReplicationMRFStats.TotalDroppedBytes", - "cmd:cmd:field:ReplicationMRFStats.TotalDroppedCount", - "cmd:cmd:field:ReplicationQueueStats.Nodes", - "cmd:cmd:field:ReplicationQueueStats.Uptime", - "cmd:cmd:field:ReplicationState.DeleteMarker", - "cmd:cmd:field:ReplicationState.PurgeTargets", - "cmd:cmd:field:ReplicationState.ReplicaStatus", - "cmd:cmd:field:ReplicationState.ReplicaTimeStamp", - "cmd:cmd:field:ReplicationState.ReplicateDecisionStr", - "cmd:cmd:field:ReplicationState.ReplicationStatusInternal", - "cmd:cmd:field:ReplicationState.ReplicationTimeStamp", - "cmd:cmd:field:ReplicationState.ResetStatusesMap", - "cmd:cmd:field:ReplicationState.Targets", - "cmd:cmd:field:ReplicationState.VersionPurgeStatusInternal", - "cmd:cmd:field:ReplicationStats.Cache", - "cmd:cmd:field:ReplicationWorkerOperation.ToMRFEntry", - "cmd:cmd:field:ResourceMetric.Avg", - "cmd:cmd:field:ResourceMetric.Count", - "cmd:cmd:field:ResourceMetric.Cumulative", - "cmd:cmd:field:ResourceMetric.Current", - "cmd:cmd:field:ResourceMetric.Labels", - "cmd:cmd:field:ResourceMetric.Max", - "cmd:cmd:field:ResourceMetric.Name", - "cmd:cmd:field:ResourceMetric.Sum", - "cmd:cmd:field:RestoreObjectRequest.Days", - "cmd:cmd:field:RestoreObjectRequest.Description", - "cmd:cmd:field:RestoreObjectRequest.OutputLocation", - "cmd:cmd:field:RestoreObjectRequest.SelectParameters", - "cmd:cmd:field:RestoreObjectRequest.Tier", - "cmd:cmd:field:RestoreObjectRequest.Type", - "cmd:cmd:field:RestoreObjectRequest.XMLName", - "cmd:cmd:field:ResyncTarget.Arn", - "cmd:cmd:field:ResyncTarget.Bucket", - "cmd:cmd:field:ResyncTarget.EndTime", - "cmd:cmd:field:ResyncTarget.FailedCount", - "cmd:cmd:field:ResyncTarget.FailedSize", - "cmd:cmd:field:ResyncTarget.Object", - "cmd:cmd:field:ResyncTarget.ReplicatedCount", - "cmd:cmd:field:ResyncTarget.ReplicatedSize", - "cmd:cmd:field:ResyncTarget.ResetID", - "cmd:cmd:field:ResyncTarget.ResyncStatus", - "cmd:cmd:field:ResyncTarget.StartTime", - "cmd:cmd:field:ResyncTargetDecision.Replicate", - "cmd:cmd:field:ResyncTargetDecision.ResetBeforeDate", - "cmd:cmd:field:ResyncTargetDecision.ResetID", - "cmd:cmd:field:ResyncTargetsInfo.Targets", - "cmd:cmd:field:S3Location.BucketName", - "cmd:cmd:field:S3Location.Encryption", - "cmd:cmd:field:S3Location.Prefix", - "cmd:cmd:field:S3Location.StorageClass", - "cmd:cmd:field:S3Location.Tagging", - "cmd:cmd:field:S3Location.UserMetadata", - "cmd:cmd:field:SMA.CAvg", - "cmd:cmd:field:SRError.Cause", - "cmd:cmd:field:SRError.Code", - "cmd:cmd:field:SRMetric.DeploymentID", - "cmd:cmd:field:SRMetric.Endpoint", - "cmd:cmd:field:SRMetric.Failed", - "cmd:cmd:field:SRMetric.LastOnline", - "cmd:cmd:field:SRMetric.Latency", - "cmd:cmd:field:SRMetric.Online", - "cmd:cmd:field:SRMetric.ReplicatedCount", - "cmd:cmd:field:SRMetric.ReplicatedSize", - "cmd:cmd:field:SRMetric.TotalDowntime", - "cmd:cmd:field:SRMetric.XferStats", - "cmd:cmd:field:SRMetricsSummary.ActiveWorkers", - "cmd:cmd:field:SRMetricsSummary.Metrics", - "cmd:cmd:field:SRMetricsSummary.Proxied", - "cmd:cmd:field:SRMetricsSummary.Queued", - "cmd:cmd:field:SRMetricsSummary.ReplicaCount", - "cmd:cmd:field:SRMetricsSummary.ReplicaSize", - "cmd:cmd:field:SRMetricsSummary.Uptime", - "cmd:cmd:field:SRStats.M", - "cmd:cmd:field:SRStats.ReplicaCount", - "cmd:cmd:field:SRStats.ReplicaSize", - "cmd:cmd:field:SRStatus.Endpoint", - "cmd:cmd:field:SRStatus.Failed", - "cmd:cmd:field:SRStatus.Latency", - "cmd:cmd:field:SRStatus.ReplicatedCount", - "cmd:cmd:field:SRStatus.ReplicatedSize", - "cmd:cmd:field:SRStatus.Secure", - "cmd:cmd:field:SRStatus.XferRateLrg", - "cmd:cmd:field:SRStatus.XferRateSml", - "cmd:cmd:field:STSError.Code", - "cmd:cmd:field:STSError.Description", - "cmd:cmd:field:STSError.HTTPStatusCode", - "cmd:cmd:field:STSErrorResponse.Error", - "cmd:cmd:field:STSErrorResponse.RequestID", - "cmd:cmd:field:STSErrorResponse.XMLName", - "cmd:cmd:field:ServerHTTPAPIStats.APIStats", - "cmd:cmd:field:ServerHTTPStats.CurrentS3Requests", - "cmd:cmd:field:ServerHTTPStats.S3RequestsInQueue", - "cmd:cmd:field:ServerHTTPStats.S3RequestsIncoming", - "cmd:cmd:field:ServerHTTPStats.TotalS34xxErrors", - "cmd:cmd:field:ServerHTTPStats.TotalS35xxErrors", - "cmd:cmd:field:ServerHTTPStats.TotalS3Canceled", - "cmd:cmd:field:ServerHTTPStats.TotalS3Errors", - "cmd:cmd:field:ServerHTTPStats.TotalS3RejectedAuth", - "cmd:cmd:field:ServerHTTPStats.TotalS3RejectedHeader", - "cmd:cmd:field:ServerHTTPStats.TotalS3RejectedInvalid", - "cmd:cmd:field:ServerHTTPStats.TotalS3RejectedTime", - "cmd:cmd:field:ServerHTTPStats.TotalS3Requests", - "cmd:cmd:field:ServerProperties.CommitID", - "cmd:cmd:field:ServerProperties.DeploymentID", - "cmd:cmd:field:ServerProperties.Region", - "cmd:cmd:field:ServerProperties.SQSARN", - "cmd:cmd:field:ServerProperties.Uptime", - "cmd:cmd:field:ServerProperties.Version", - "cmd:cmd:field:ServerSystemConfig.Checksum", - "cmd:cmd:field:ServerSystemConfig.CmdLines", - "cmd:cmd:field:ServerSystemConfig.MinioEnv", - "cmd:cmd:field:ServerSystemConfig.NEndpoints", - "cmd:cmd:field:SiteResyncStatus.BucketStatuses", - "cmd:cmd:field:SiteResyncStatus.DeplID", - "cmd:cmd:field:SiteResyncStatus.Status", - "cmd:cmd:field:SiteResyncStatus.TotBuckets", - "cmd:cmd:field:SiteResyncStatus.Version", - "cmd:cmd:field:SpeedTestResult.DownloadTTFB", - "cmd:cmd:field:SpeedTestResult.DownloadTimes", - "cmd:cmd:field:SpeedTestResult.Downloads", - "cmd:cmd:field:SpeedTestResult.Endpoint", - "cmd:cmd:field:SpeedTestResult.Error", - "cmd:cmd:field:SpeedTestResult.UploadTimes", - "cmd:cmd:field:SpeedTestResult.Uploads", - "cmd:cmd:field:StartProfilingResult.Error", - "cmd:cmd:field:StartProfilingResult.NodeName", - "cmd:cmd:field:StartProfilingResult.Success", - "cmd:cmd:field:StatInfo.Dir", - "cmd:cmd:field:StatInfo.ModTime", - "cmd:cmd:field:StatInfo.Mode", - "cmd:cmd:field:StatInfo.Name", - "cmd:cmd:field:StatInfo.Size", - "cmd:cmd:field:StorageAPI.AppendFile", - "cmd:cmd:field:StorageAPI.CheckParts", - "cmd:cmd:field:StorageAPI.CleanAbandonedData", - "cmd:cmd:field:StorageAPI.Close", - "cmd:cmd:field:StorageAPI.CreateFile", - "cmd:cmd:field:StorageAPI.Delete", - "cmd:cmd:field:StorageAPI.DeleteBulk", - "cmd:cmd:field:StorageAPI.DeleteVersion", - "cmd:cmd:field:StorageAPI.DeleteVersions", - "cmd:cmd:field:StorageAPI.DeleteVol", - "cmd:cmd:field:StorageAPI.DiskInfo", - "cmd:cmd:field:StorageAPI.Endpoint", - "cmd:cmd:field:StorageAPI.GetDiskID", - "cmd:cmd:field:StorageAPI.GetDiskLoc", - "cmd:cmd:field:StorageAPI.Healing", - "cmd:cmd:field:StorageAPI.Hostname", - "cmd:cmd:field:StorageAPI.IsLocal", - "cmd:cmd:field:StorageAPI.IsOnline", - "cmd:cmd:field:StorageAPI.LastConn", - "cmd:cmd:field:StorageAPI.ListDir", - "cmd:cmd:field:StorageAPI.ListVols", - "cmd:cmd:field:StorageAPI.MakeVol", - "cmd:cmd:field:StorageAPI.MakeVolBulk", - "cmd:cmd:field:StorageAPI.NSScanner", - "cmd:cmd:field:StorageAPI.ReadAll", - "cmd:cmd:field:StorageAPI.ReadFile", - "cmd:cmd:field:StorageAPI.ReadFileStream", - "cmd:cmd:field:StorageAPI.ReadParts", - "cmd:cmd:field:StorageAPI.ReadVersion", - "cmd:cmd:field:StorageAPI.ReadXL", - "cmd:cmd:field:StorageAPI.RenameData", - "cmd:cmd:field:StorageAPI.RenameFile", - "cmd:cmd:field:StorageAPI.RenamePart", - "cmd:cmd:field:StorageAPI.SetDiskID", - "cmd:cmd:field:StorageAPI.StatInfoFile", - "cmd:cmd:field:StorageAPI.StatVol", - "cmd:cmd:field:StorageAPI.String", - "cmd:cmd:field:StorageAPI.UpdateMetadata", - "cmd:cmd:field:StorageAPI.VerifyFile", - "cmd:cmd:field:StorageAPI.WalkDir", - "cmd:cmd:field:StorageAPI.WriteAll", - "cmd:cmd:field:StorageAPI.WriteMetadata", - "cmd:cmd:field:TargetClient.ARN", - "cmd:cmd:field:TargetClient.Bucket", - "cmd:cmd:field:TargetClient.Endpoint", - "cmd:cmd:field:TargetClient.ResetID", - "cmd:cmd:field:TargetClient.Secure", - "cmd:cmd:field:TargetClient.StorageClass", - "cmd:cmd:field:TargetReplicationResyncStatus.Bucket", - "cmd:cmd:field:TargetReplicationResyncStatus.Error", - "cmd:cmd:field:TargetReplicationResyncStatus.FailedCount", - "cmd:cmd:field:TargetReplicationResyncStatus.FailedSize", - "cmd:cmd:field:TargetReplicationResyncStatus.LastUpdate", - "cmd:cmd:field:TargetReplicationResyncStatus.Object", - "cmd:cmd:field:TargetReplicationResyncStatus.ReplicatedCount", - "cmd:cmd:field:TargetReplicationResyncStatus.ReplicatedSize", - "cmd:cmd:field:TargetReplicationResyncStatus.ResyncBeforeDate", - "cmd:cmd:field:TargetReplicationResyncStatus.ResyncID", - "cmd:cmd:field:TargetReplicationResyncStatus.ResyncStatus", - "cmd:cmd:field:TargetReplicationResyncStatus.StartTime", - "cmd:cmd:field:TierConfigMgr.Tiers", - "cmd:cmd:field:TransitionOptions.ETag", - "cmd:cmd:field:TransitionOptions.ExpireRestored", - "cmd:cmd:field:TransitionOptions.RestoreExpiry", - "cmd:cmd:field:TransitionOptions.RestoreRequest", - "cmd:cmd:field:TransitionOptions.Status", - "cmd:cmd:field:TransitionOptions.Tier", - "cmd:cmd:field:TransitionedObject.FreeVersion", - "cmd:cmd:field:TransitionedObject.Name", - "cmd:cmd:field:TransitionedObject.Status", - "cmd:cmd:field:TransitionedObject.Tier", - "cmd:cmd:field:TransitionedObject.VersionID", - "cmd:cmd:field:UpdateMetadataOpts.NoPersistence", - "cmd:cmd:field:Upload.Initiated", - "cmd:cmd:field:Upload.Initiator", - "cmd:cmd:field:Upload.Key", - "cmd:cmd:field:Upload.Owner", - "cmd:cmd:field:Upload.StorageClass", - "cmd:cmd:field:Upload.UploadID", - "cmd:cmd:field:UserIdentity.Credentials", - "cmd:cmd:field:UserIdentity.UpdatedAt", - "cmd:cmd:field:UserIdentity.Version", - "cmd:cmd:field:VolInfo.Created", - "cmd:cmd:field:VolInfo.Deleted", - "cmd:cmd:field:VolInfo.Name", - "cmd:cmd:field:WalkDirOptions.BaseDir", - "cmd:cmd:field:WalkDirOptions.Bucket", - "cmd:cmd:field:WalkDirOptions.DiskID", - "cmd:cmd:field:WalkDirOptions.FilterPrefix", - "cmd:cmd:field:WalkDirOptions.ForwardTo", - "cmd:cmd:field:WalkDirOptions.Limit", - "cmd:cmd:field:WalkDirOptions.Recursive", - "cmd:cmd:field:WalkDirOptions.ReportNotFound", - "cmd:cmd:field:WalkOptions.AskDisks", - "cmd:cmd:field:WalkOptions.Filter", - "cmd:cmd:field:WalkOptions.LatestOnly", - "cmd:cmd:field:WalkOptions.Limit", - "cmd:cmd:field:WalkOptions.Marker", - "cmd:cmd:field:WalkOptions.VersionsSort", - "cmd:cmd:field:WarmBackend.Get", - "cmd:cmd:field:WarmBackend.InUse", - "cmd:cmd:field:WarmBackend.Put", - "cmd:cmd:field:WarmBackend.PutWithMeta", - "cmd:cmd:field:WarmBackend.Remove", - "cmd:cmd:field:WebIdentityResult.AssumedRoleUser", - "cmd:cmd:field:WebIdentityResult.Audience", - "cmd:cmd:field:WebIdentityResult.Credentials", - "cmd:cmd:field:WebIdentityResult.PackedPolicySize", - "cmd:cmd:field:WebIdentityResult.Provider", - "cmd:cmd:field:WebIdentityResult.SubjectFromWebIdentityToken", - "cmd:cmd:field:WriteAllHandlerParams.Buf", - "cmd:cmd:field:WriteAllHandlerParams.DiskID", - "cmd:cmd:field:WriteAllHandlerParams.FilePath", - "cmd:cmd:field:WriteAllHandlerParams.Volume", - "cmd:cmd:field:XferStats.Avg", - "cmd:cmd:field:XferStats.Curr", - "cmd:cmd:field:XferStats.N", - "cmd:cmd:field:XferStats.Peak", - "cmd:cmd:func:Access", - "cmd:cmd:func:AuthMiddleware", - "cmd:cmd:func:BitrotAlgorithmFromString", - "cmd:cmd:func:BucketAccessPolicyToPolicy", - "cmd:cmd:func:CheckLocalServerAddr", - "cmd:cmd:func:ClusterCheckHandler", - "cmd:cmd:func:ClusterReadCheckHandler", - "cmd:cmd:func:Create", - "cmd:cmd:func:CreatePoolEndpoints", - "cmd:cmd:func:DecryptBlocksRequestR", - "cmd:cmd:func:DecryptCopyRequestR", - "cmd:cmd:func:DecryptETag", - "cmd:cmd:func:DecryptETags", - "cmd:cmd:func:DecryptObjectInfo", - "cmd:cmd:func:DecryptRequestWithSequenceNumberR", - "cmd:cmd:func:EncryptRequest", - "cmd:cmd:func:ErrorRespToObjectError", - "cmd:cmd:func:Fdatasync", - "cmd:cmd:func:GenETag", - "cmd:cmd:func:GetAllSets", - "cmd:cmd:func:GetCurrentReleaseTime", - "cmd:cmd:func:GetDefaultConnSettings", - "cmd:cmd:func:GetHelp", - "cmd:cmd:func:GetInternalReplicationState", - "cmd:cmd:func:GetLocalPeer", - "cmd:cmd:func:GetObject", - "cmd:cmd:func:GetProxyEndpointLocalIndex", - "cmd:cmd:func:GetProxyEndpoints", - "cmd:cmd:func:GetTotalCapacity", - "cmd:cmd:func:GetTotalCapacityFree", - "cmd:cmd:func:GetTotalUsableCapacity", - "cmd:cmd:func:GetTotalUsableCapacityFree", - "cmd:cmd:func:HasPrefix", - "cmd:cmd:func:HasSuffix", - "cmd:cmd:func:IsBOSH", - "cmd:cmd:func:IsDCOS", - "cmd:cmd:func:IsDocker", - "cmd:cmd:func:IsErr", - "cmd:cmd:func:IsErrIgnored", - "cmd:cmd:func:IsKubernetes", - "cmd:cmd:func:IsPCFTile", - "cmd:cmd:func:IsSourceBuild", - "cmd:cmd:func:IsValidBucketName", - "cmd:cmd:func:IsValidObjectName", - "cmd:cmd:func:IsValidObjectPrefix", - "cmd:cmd:func:JoinBucketLoaders", - "cmd:cmd:func:JoinLoaders", - "cmd:cmd:func:LivenessCheckHandler", - "cmd:cmd:func:Load", - "cmd:cmd:func:Lstat", - "cmd:cmd:func:Main", - "cmd:cmd:func:Mkdir", - "cmd:cmd:func:MkdirAll", - "cmd:cmd:func:MockOpenIDTestUserInteraction", - "cmd:cmd:func:NewBitrotVerifier", - "cmd:cmd:func:NewBucketMetadataSys", - "cmd:cmd:func:NewBucketMetricsGroup", - "cmd:cmd:func:NewBucketObjectLockSys", - "cmd:cmd:func:NewBucketQuotaSys", - "cmd:cmd:func:NewBucketSSEConfigSys", - "cmd:cmd:func:NewBucketTargetSys", - "cmd:cmd:func:NewBucketVersioningSys", - "cmd:cmd:func:NewConfigSys", - "cmd:cmd:func:NewConsoleLogger", - "cmd:cmd:func:NewCounterMD", - "cmd:cmd:func:NewEndpoint", - "cmd:cmd:func:NewEndpoints", - "cmd:cmd:func:NewErasure", - "cmd:cmd:func:NewEventNotifier", - "cmd:cmd:func:NewFTPDriver", - "cmd:cmd:func:NewGaugeMD", - "cmd:cmd:func:NewGetObjectReader", - "cmd:cmd:func:NewGetObjectReaderFromReader", - "cmd:cmd:func:NewHTTPTransport", - "cmd:cmd:func:NewHTTPTransportWithClientCerts", - "cmd:cmd:func:NewHTTPTransportWithTimeout", - "cmd:cmd:func:NewIAMSys", - "cmd:cmd:func:NewInternodeHTTPTransport", - "cmd:cmd:func:NewLifecycleSys", - "cmd:cmd:func:NewMetricsGroup", - "cmd:cmd:func:NewNotificationSys", - "cmd:cmd:func:NewPolicySys", - "cmd:cmd:func:NewPutObjReader", - "cmd:cmd:func:NewRemoteTargetHTTPTransport", - "cmd:cmd:func:NewReplicationPool", - "cmd:cmd:func:NewReplicationStats", - "cmd:cmd:func:NewS3PeerSys", - "cmd:cmd:func:NewSFTPDriver", - "cmd:cmd:func:NewTierConfigMgr", - "cmd:cmd:func:NoAuthMiddleware", - "cmd:cmd:func:Open", - "cmd:cmd:func:OpenFile", - "cmd:cmd:func:OpenFileDirectIO", - "cmd:cmd:func:ParseSSECopyCustomerRequest", - "cmd:cmd:func:ParseSSECustomerHeader", - "cmd:cmd:func:ParseSSECustomerRequest", - "cmd:cmd:func:PolicyToBucketAccessPolicy", - "cmd:cmd:func:QueueReplicationHeal", - "cmd:cmd:func:ReadinessCheckHandler", - "cmd:cmd:func:Remove", - "cmd:cmd:func:RemoveAll", - "cmd:cmd:func:Rename", - "cmd:cmd:func:RenameSys", - "cmd:cmd:func:ReportMetrics", - "cmd:cmd:func:Save", - "cmd:cmd:func:SetHistogramValues", - "cmd:cmd:func:Stat", - "cmd:cmd:func:ToS3ETag", - "cmd:cmd:func:UTCNow", - "cmd:cmd:func:WithNPeers", - "cmd:cmd:func:WithNPeersThrottled", - "cmd:cmd:method:APIErrorCode.String", - "cmd:cmd:method:AccElem.DecodeMsg", - "cmd:cmd:method:AccElem.EncodeMsg", - "cmd:cmd:method:AccElem.MarshalMsg", - "cmd:cmd:method:AccElem.Msgsize", - "cmd:cmd:method:AccElem.UnmarshalMsg", - "cmd:cmd:method:ActiveWorkerStat.DecodeMsg", - "cmd:cmd:method:ActiveWorkerStat.EncodeMsg", - "cmd:cmd:method:ActiveWorkerStat.MarshalMsg", - "cmd:cmd:method:ActiveWorkerStat.Msgsize", - "cmd:cmd:method:ActiveWorkerStat.UnmarshalMsg", - "cmd:cmd:method:AdminError.Error", - "cmd:cmd:method:AllAccessDisabled.Error", - "cmd:cmd:method:BackendDown.Error", - "cmd:cmd:method:BackendType.MarshalMsg", - "cmd:cmd:method:BackendType.Msgsize", - "cmd:cmd:method:BackendType.UnmarshalMsg", - "cmd:cmd:method:BaseOptions.DecodeMsg", - "cmd:cmd:method:BaseOptions.EncodeMsg", - "cmd:cmd:method:BaseOptions.MarshalMsg", - "cmd:cmd:method:BaseOptions.Msgsize", - "cmd:cmd:method:BaseOptions.UnmarshalMsg", - "cmd:cmd:method:BatchJobExpire.DecodeMsg", - "cmd:cmd:method:BatchJobExpire.EncodeMsg", - "cmd:cmd:method:BatchJobExpire.Expire", - "cmd:cmd:method:BatchJobExpire.MarshalMsg", - "cmd:cmd:method:BatchJobExpire.Msgsize", - "cmd:cmd:method:BatchJobExpire.Notify", - "cmd:cmd:method:BatchJobExpire.RedactSensitive", - "cmd:cmd:method:BatchJobExpire.Start", - "cmd:cmd:method:BatchJobExpire.UnmarshalMsg", - "cmd:cmd:method:BatchJobExpire.UnmarshalYAML", - "cmd:cmd:method:BatchJobExpire.Validate", - "cmd:cmd:method:BatchJobExpireFilter.DecodeMsg", - "cmd:cmd:method:BatchJobExpireFilter.EncodeMsg", - "cmd:cmd:method:BatchJobExpireFilter.MarshalMsg", - "cmd:cmd:method:BatchJobExpireFilter.Matches", - "cmd:cmd:method:BatchJobExpireFilter.Msgsize", - "cmd:cmd:method:BatchJobExpireFilter.UnmarshalMsg", - "cmd:cmd:method:BatchJobExpireFilter.UnmarshalYAML", - "cmd:cmd:method:BatchJobExpireFilter.Validate", - "cmd:cmd:method:BatchJobExpirePurge.DecodeMsg", - "cmd:cmd:method:BatchJobExpirePurge.EncodeMsg", - "cmd:cmd:method:BatchJobExpirePurge.MarshalMsg", - "cmd:cmd:method:BatchJobExpirePurge.Msgsize", - "cmd:cmd:method:BatchJobExpirePurge.UnmarshalMsg", - "cmd:cmd:method:BatchJobExpirePurge.UnmarshalYAML", - "cmd:cmd:method:BatchJobExpirePurge.Validate", - "cmd:cmd:method:BatchJobKV.DecodeMsg", - "cmd:cmd:method:BatchJobKV.Empty", - "cmd:cmd:method:BatchJobKV.EncodeMsg", - "cmd:cmd:method:BatchJobKV.MarshalMsg", - "cmd:cmd:method:BatchJobKV.Match", - "cmd:cmd:method:BatchJobKV.Msgsize", - "cmd:cmd:method:BatchJobKV.UnmarshalMsg", - "cmd:cmd:method:BatchJobKV.UnmarshalYAML", - "cmd:cmd:method:BatchJobKV.Validate", - "cmd:cmd:method:BatchJobKeyRotateEncryption.DecodeMsg", - "cmd:cmd:method:BatchJobKeyRotateEncryption.EncodeMsg", - "cmd:cmd:method:BatchJobKeyRotateEncryption.MarshalMsg", - "cmd:cmd:method:BatchJobKeyRotateEncryption.Msgsize", - "cmd:cmd:method:BatchJobKeyRotateEncryption.UnmarshalMsg", - "cmd:cmd:method:BatchJobKeyRotateEncryption.Validate", - "cmd:cmd:method:BatchJobKeyRotateFlags.DecodeMsg", - "cmd:cmd:method:BatchJobKeyRotateFlags.EncodeMsg", - "cmd:cmd:method:BatchJobKeyRotateFlags.MarshalMsg", - "cmd:cmd:method:BatchJobKeyRotateFlags.Msgsize", - "cmd:cmd:method:BatchJobKeyRotateFlags.UnmarshalMsg", - "cmd:cmd:method:BatchJobKeyRotateV1.DecodeMsg", - "cmd:cmd:method:BatchJobKeyRotateV1.EncodeMsg", - "cmd:cmd:method:BatchJobKeyRotateV1.KeyRotate", - "cmd:cmd:method:BatchJobKeyRotateV1.MarshalMsg", - "cmd:cmd:method:BatchJobKeyRotateV1.Msgsize", - "cmd:cmd:method:BatchJobKeyRotateV1.Notify", - "cmd:cmd:method:BatchJobKeyRotateV1.RedactSensitive", - "cmd:cmd:method:BatchJobKeyRotateV1.Start", - "cmd:cmd:method:BatchJobKeyRotateV1.UnmarshalMsg", - "cmd:cmd:method:BatchJobKeyRotateV1.Validate", - "cmd:cmd:method:BatchJobNotification.DecodeMsg", - "cmd:cmd:method:BatchJobNotification.EncodeMsg", - "cmd:cmd:method:BatchJobNotification.MarshalMsg", - "cmd:cmd:method:BatchJobNotification.Msgsize", - "cmd:cmd:method:BatchJobNotification.UnmarshalMsg", - "cmd:cmd:method:BatchJobNotification.UnmarshalYAML", - "cmd:cmd:method:BatchJobPool.AddWorker", - "cmd:cmd:method:BatchJobPool.ResizeWorkers", - "cmd:cmd:method:BatchJobPrefix.DecodeMsg", - "cmd:cmd:method:BatchJobPrefix.EncodeMsg", - "cmd:cmd:method:BatchJobPrefix.F", - "cmd:cmd:method:BatchJobPrefix.MarshalMsg", - "cmd:cmd:method:BatchJobPrefix.Msgsize", - "cmd:cmd:method:BatchJobPrefix.UnmarshalMsg", - "cmd:cmd:method:BatchJobPrefix.UnmarshalYAML", - "cmd:cmd:method:BatchJobReplicateCredentials.DecodeMsg", - "cmd:cmd:method:BatchJobReplicateCredentials.Empty", - "cmd:cmd:method:BatchJobReplicateCredentials.EncodeMsg", - "cmd:cmd:method:BatchJobReplicateCredentials.MarshalMsg", - "cmd:cmd:method:BatchJobReplicateCredentials.Msgsize", - "cmd:cmd:method:BatchJobReplicateCredentials.UnmarshalMsg", - "cmd:cmd:method:BatchJobReplicateCredentials.Validate", - "cmd:cmd:method:BatchJobReplicateFlags.DecodeMsg", - "cmd:cmd:method:BatchJobReplicateFlags.EncodeMsg", - "cmd:cmd:method:BatchJobReplicateFlags.MarshalMsg", - "cmd:cmd:method:BatchJobReplicateFlags.Msgsize", - "cmd:cmd:method:BatchJobReplicateFlags.UnmarshalMsg", - "cmd:cmd:method:BatchJobReplicateResourceType.DecodeMsg", - "cmd:cmd:method:BatchJobReplicateResourceType.EncodeMsg", - "cmd:cmd:method:BatchJobReplicateResourceType.MarshalMsg", - "cmd:cmd:method:BatchJobReplicateResourceType.Msgsize", - "cmd:cmd:method:BatchJobReplicateResourceType.UnmarshalMsg", - "cmd:cmd:method:BatchJobReplicateResourceType.Validate", - "cmd:cmd:method:BatchJobReplicateSource.DecodeMsg", - "cmd:cmd:method:BatchJobReplicateSource.EncodeMsg", - "cmd:cmd:method:BatchJobReplicateSource.MarshalMsg", - "cmd:cmd:method:BatchJobReplicateSource.Msgsize", - "cmd:cmd:method:BatchJobReplicateSource.UnmarshalMsg", - "cmd:cmd:method:BatchJobReplicateSource.ValidPath", - "cmd:cmd:method:BatchJobReplicateTarget.DecodeMsg", - "cmd:cmd:method:BatchJobReplicateTarget.EncodeMsg", - "cmd:cmd:method:BatchJobReplicateTarget.MarshalMsg", - "cmd:cmd:method:BatchJobReplicateTarget.Msgsize", - "cmd:cmd:method:BatchJobReplicateTarget.UnmarshalMsg", - "cmd:cmd:method:BatchJobReplicateTarget.ValidPath", - "cmd:cmd:method:BatchJobReplicateV1.DecodeMsg", - "cmd:cmd:method:BatchJobReplicateV1.EncodeMsg", - "cmd:cmd:method:BatchJobReplicateV1.MarshalMsg", - "cmd:cmd:method:BatchJobReplicateV1.Msgsize", - "cmd:cmd:method:BatchJobReplicateV1.Notify", - "cmd:cmd:method:BatchJobReplicateV1.RedactSensitive", - "cmd:cmd:method:BatchJobReplicateV1.RemoteToLocal", - "cmd:cmd:method:BatchJobReplicateV1.ReplicateFromSource", - "cmd:cmd:method:BatchJobReplicateV1.ReplicateToTarget", - "cmd:cmd:method:BatchJobReplicateV1.Start", - "cmd:cmd:method:BatchJobReplicateV1.StartFromSource", - "cmd:cmd:method:BatchJobReplicateV1.UnmarshalMsg", - "cmd:cmd:method:BatchJobReplicateV1.Validate", - "cmd:cmd:method:BatchJobRequest.DecodeMsg", - "cmd:cmd:method:BatchJobRequest.EncodeMsg", - "cmd:cmd:method:BatchJobRequest.MarshalMsg", - "cmd:cmd:method:BatchJobRequest.Msgsize", - "cmd:cmd:method:BatchJobRequest.RedactSensitive", - "cmd:cmd:method:BatchJobRequest.Type", - "cmd:cmd:method:BatchJobRequest.UnmarshalMsg", - "cmd:cmd:method:BatchJobRequest.Validate", - "cmd:cmd:method:BatchJobRetry.DecodeMsg", - "cmd:cmd:method:BatchJobRetry.EncodeMsg", - "cmd:cmd:method:BatchJobRetry.MarshalMsg", - "cmd:cmd:method:BatchJobRetry.Msgsize", - "cmd:cmd:method:BatchJobRetry.UnmarshalMsg", - "cmd:cmd:method:BatchJobRetry.UnmarshalYAML", - "cmd:cmd:method:BatchJobRetry.Validate", - "cmd:cmd:method:BatchJobSize.DecodeMsg", - "cmd:cmd:method:BatchJobSize.EncodeMsg", - "cmd:cmd:method:BatchJobSize.MarshalMsg", - "cmd:cmd:method:BatchJobSize.Msgsize", - "cmd:cmd:method:BatchJobSize.UnmarshalMsg", - "cmd:cmd:method:BatchJobSize.UnmarshalYAML", - "cmd:cmd:method:BatchJobSizeFilter.DecodeMsg", - "cmd:cmd:method:BatchJobSizeFilter.EncodeMsg", - "cmd:cmd:method:BatchJobSizeFilter.InRange", - "cmd:cmd:method:BatchJobSizeFilter.MarshalMsg", - "cmd:cmd:method:BatchJobSizeFilter.Msgsize", - "cmd:cmd:method:BatchJobSizeFilter.UnmarshalMsg", - "cmd:cmd:method:BatchJobSizeFilter.UnmarshalYAML", - "cmd:cmd:method:BatchJobSizeFilter.Validate", - "cmd:cmd:method:BatchJobSnowball.DecodeMsg", - "cmd:cmd:method:BatchJobSnowball.EncodeMsg", - "cmd:cmd:method:BatchJobSnowball.MarshalMsg", - "cmd:cmd:method:BatchJobSnowball.Msgsize", - "cmd:cmd:method:BatchJobSnowball.UnmarshalMsg", - "cmd:cmd:method:BatchJobSnowball.UnmarshalYAML", - "cmd:cmd:method:BatchJobSnowball.Validate", - "cmd:cmd:method:BatchJobYamlErr.Error", - "cmd:cmd:method:BatchKeyRotateFilter.DecodeMsg", - "cmd:cmd:method:BatchKeyRotateFilter.EncodeMsg", - "cmd:cmd:method:BatchKeyRotateFilter.MarshalMsg", - "cmd:cmd:method:BatchKeyRotateFilter.Msgsize", - "cmd:cmd:method:BatchKeyRotateFilter.UnmarshalMsg", - "cmd:cmd:method:BatchKeyRotateNotification.DecodeMsg", - "cmd:cmd:method:BatchKeyRotateNotification.EncodeMsg", - "cmd:cmd:method:BatchKeyRotateNotification.MarshalMsg", - "cmd:cmd:method:BatchKeyRotateNotification.Msgsize", - "cmd:cmd:method:BatchKeyRotateNotification.UnmarshalMsg", - "cmd:cmd:method:BatchKeyRotationType.DecodeMsg", - "cmd:cmd:method:BatchKeyRotationType.EncodeMsg", - "cmd:cmd:method:BatchKeyRotationType.MarshalMsg", - "cmd:cmd:method:BatchKeyRotationType.Msgsize", - "cmd:cmd:method:BatchKeyRotationType.UnmarshalMsg", - "cmd:cmd:method:BatchReplicateFilter.DecodeMsg", - "cmd:cmd:method:BatchReplicateFilter.EncodeMsg", - "cmd:cmd:method:BatchReplicateFilter.MarshalMsg", - "cmd:cmd:method:BatchReplicateFilter.Msgsize", - "cmd:cmd:method:BatchReplicateFilter.UnmarshalMsg", - "cmd:cmd:method:BitrotAlgorithm.Available", - "cmd:cmd:method:BitrotAlgorithm.DecodeMsg", - "cmd:cmd:method:BitrotAlgorithm.EncodeMsg", - "cmd:cmd:method:BitrotAlgorithm.MarshalMsg", - "cmd:cmd:method:BitrotAlgorithm.Msgsize", - "cmd:cmd:method:BitrotAlgorithm.New", - "cmd:cmd:method:BitrotAlgorithm.String", - "cmd:cmd:method:BitrotAlgorithm.UnmarshalMsg", - "cmd:cmd:method:BucketAlreadyExists.Error", - "cmd:cmd:method:BucketAlreadyOwnedByYou.Error", - "cmd:cmd:method:BucketExists.Error", - "cmd:cmd:method:BucketInfo.MarshalMsg", - "cmd:cmd:method:BucketInfo.Msgsize", - "cmd:cmd:method:BucketInfo.UnmarshalMsg", - "cmd:cmd:method:BucketLifecycleNotFound.Error", - "cmd:cmd:method:BucketMetadata.DecodeMsg", - "cmd:cmd:method:BucketMetadata.EncodeMsg", - "cmd:cmd:method:BucketMetadata.MarshalMsg", - "cmd:cmd:method:BucketMetadata.Msgsize", - "cmd:cmd:method:BucketMetadata.ObjectLocking", - "cmd:cmd:method:BucketMetadata.Save", - "cmd:cmd:method:BucketMetadata.SetCreatedAt", - "cmd:cmd:method:BucketMetadata.UnmarshalMsg", - "cmd:cmd:method:BucketMetadata.Versioning", - "cmd:cmd:method:BucketMetadataSys.Count", - "cmd:cmd:method:BucketMetadataSys.CreatedAt", - "cmd:cmd:method:BucketMetadataSys.Delete", - "cmd:cmd:method:BucketMetadataSys.Get", - "cmd:cmd:method:BucketMetadataSys.GetBucketPolicy", - "cmd:cmd:method:BucketMetadataSys.GetBucketTargetsConfig", - "cmd:cmd:method:BucketMetadataSys.GetConfig", - "cmd:cmd:method:BucketMetadataSys.GetConfigFromDisk", - "cmd:cmd:method:BucketMetadataSys.GetLifecycleConfig", - "cmd:cmd:method:BucketMetadataSys.GetNotificationConfig", - "cmd:cmd:method:BucketMetadataSys.GetObjectLockConfig", - "cmd:cmd:method:BucketMetadataSys.GetPolicyConfig", - "cmd:cmd:method:BucketMetadataSys.GetQuotaConfig", - "cmd:cmd:method:BucketMetadataSys.GetReplicationConfig", - "cmd:cmd:method:BucketMetadataSys.GetSSEConfig", - "cmd:cmd:method:BucketMetadataSys.GetTaggingConfig", - "cmd:cmd:method:BucketMetadataSys.GetVersioningConfig", - "cmd:cmd:method:BucketMetadataSys.Init", - "cmd:cmd:method:BucketMetadataSys.Initialized", - "cmd:cmd:method:BucketMetadataSys.Remove", - "cmd:cmd:method:BucketMetadataSys.RemoveStaleBuckets", - "cmd:cmd:method:BucketMetadataSys.Reset", - "cmd:cmd:method:BucketMetadataSys.Set", - "cmd:cmd:method:BucketMetadataSys.Update", - "cmd:cmd:method:BucketNameInvalid.Error", - "cmd:cmd:method:BucketNotEmpty.Error", - "cmd:cmd:method:BucketNotFound.Error", - "cmd:cmd:method:BucketObjectLockConfigNotFound.Error", - "cmd:cmd:method:BucketObjectLockSys.Get", - "cmd:cmd:method:BucketOptions.MarshalMsg", - "cmd:cmd:method:BucketOptions.Msgsize", - "cmd:cmd:method:BucketOptions.UnmarshalMsg", - "cmd:cmd:method:BucketPolicyNotFound.Error", - "cmd:cmd:method:BucketQuotaConfigNotFound.Error", - "cmd:cmd:method:BucketQuotaExceeded.Error", - "cmd:cmd:method:BucketQuotaSys.Get", - "cmd:cmd:method:BucketQuotaSys.GetBucketUsageInfo", - "cmd:cmd:method:BucketQuotaSys.Init", - "cmd:cmd:method:BucketRemoteAlreadyExists.Error", - "cmd:cmd:method:BucketRemoteArnInvalid.Error", - "cmd:cmd:method:BucketRemoteArnTypeInvalid.Error", - "cmd:cmd:method:BucketRemoteDestinationNotFound.Error", - "cmd:cmd:method:BucketRemoteIdenticalToSource.Error", - "cmd:cmd:method:BucketRemoteLabelInUse.Error", - "cmd:cmd:method:BucketRemoteRemoveDisallowed.Error", - "cmd:cmd:method:BucketRemoteTargetNotFound.Error", - "cmd:cmd:method:BucketRemoteTargetNotVersioned.Error", - "cmd:cmd:method:BucketReplicationConfigNotFound.Error", - "cmd:cmd:method:BucketReplicationResyncStatus.DecodeMsg", - "cmd:cmd:method:BucketReplicationResyncStatus.EncodeMsg", - "cmd:cmd:method:BucketReplicationResyncStatus.MarshalMsg", - "cmd:cmd:method:BucketReplicationResyncStatus.Msgsize", - "cmd:cmd:method:BucketReplicationResyncStatus.UnmarshalMsg", - "cmd:cmd:method:BucketReplicationSourceNotVersioned.Error", - "cmd:cmd:method:BucketReplicationStat.DecodeMsg", - "cmd:cmd:method:BucketReplicationStat.EncodeMsg", - "cmd:cmd:method:BucketReplicationStat.MarshalMsg", - "cmd:cmd:method:BucketReplicationStat.Msgsize", - "cmd:cmd:method:BucketReplicationStat.UnmarshalMsg", - "cmd:cmd:method:BucketReplicationStats.Clone", - "cmd:cmd:method:BucketReplicationStats.DecodeMsg", - "cmd:cmd:method:BucketReplicationStats.Empty", - "cmd:cmd:method:BucketReplicationStats.EncodeMsg", - "cmd:cmd:method:BucketReplicationStats.MarshalMsg", - "cmd:cmd:method:BucketReplicationStats.Msgsize", - "cmd:cmd:method:BucketReplicationStats.UnmarshalMsg", - "cmd:cmd:method:BucketSSEConfigNotFound.Error", - "cmd:cmd:method:BucketSSEConfigSys.Get", - "cmd:cmd:method:BucketStats.DecodeMsg", - "cmd:cmd:method:BucketStats.EncodeMsg", - "cmd:cmd:method:BucketStats.MarshalMsg", - "cmd:cmd:method:BucketStats.Msgsize", - "cmd:cmd:method:BucketStats.UnmarshalMsg", - "cmd:cmd:method:BucketStatsMap.DecodeMsg", - "cmd:cmd:method:BucketStatsMap.EncodeMsg", - "cmd:cmd:method:BucketStatsMap.MarshalMsg", - "cmd:cmd:method:BucketStatsMap.Msgsize", - "cmd:cmd:method:BucketStatsMap.UnmarshalMsg", - "cmd:cmd:method:BucketTaggingNotFound.Error", - "cmd:cmd:method:BucketTargetSys.Delete", - "cmd:cmd:method:BucketTargetSys.GetRemoteBucketTargetByArn", - "cmd:cmd:method:BucketTargetSys.GetRemoteTargetClient", - "cmd:cmd:method:BucketTargetSys.ListBucketTargets", - "cmd:cmd:method:BucketTargetSys.ListTargets", - "cmd:cmd:method:BucketTargetSys.RemoveTarget", - "cmd:cmd:method:BucketTargetSys.SetTarget", - "cmd:cmd:method:BucketTargetSys.UpdateAllTargets", - "cmd:cmd:method:BucketVersioningSys.Enabled", - "cmd:cmd:method:BucketVersioningSys.Get", - "cmd:cmd:method:BucketVersioningSys.PrefixEnabled", - "cmd:cmd:method:BucketVersioningSys.PrefixSuspended", - "cmd:cmd:method:BucketVersioningSys.Suspended", - "cmd:cmd:method:CheckPartsHandlerParams.DecodeMsg", - "cmd:cmd:method:CheckPartsHandlerParams.EncodeMsg", - "cmd:cmd:method:CheckPartsHandlerParams.MarshalMsg", - "cmd:cmd:method:CheckPartsHandlerParams.Msgsize", - "cmd:cmd:method:CheckPartsHandlerParams.UnmarshalMsg", - "cmd:cmd:method:CheckPartsResp.DecodeMsg", - "cmd:cmd:method:CheckPartsResp.EncodeMsg", - "cmd:cmd:method:CheckPartsResp.MarshalMsg", - "cmd:cmd:method:CheckPartsResp.Msgsize", - "cmd:cmd:method:CheckPartsResp.UnmarshalMsg", - "cmd:cmd:method:ChecksumAlgo.DecodeMsg", - "cmd:cmd:method:ChecksumAlgo.EncodeMsg", - "cmd:cmd:method:ChecksumAlgo.MarshalMsg", - "cmd:cmd:method:ChecksumAlgo.Msgsize", - "cmd:cmd:method:ChecksumAlgo.UnmarshalMsg", - "cmd:cmd:method:ChecksumInfo.DecodeMsg", - "cmd:cmd:method:ChecksumInfo.EncodeMsg", - "cmd:cmd:method:ChecksumInfo.MarshalJSON", - "cmd:cmd:method:ChecksumInfo.MarshalMsg", - "cmd:cmd:method:ChecksumInfo.Msgsize", - "cmd:cmd:method:ChecksumInfo.UnmarshalJSON", - "cmd:cmd:method:ChecksumInfo.UnmarshalMsg", - "cmd:cmd:method:CompleteMultipartUpload.MarshalMsg", - "cmd:cmd:method:CompleteMultipartUpload.Msgsize", - "cmd:cmd:method:CompleteMultipartUpload.UnmarshalMsg", - "cmd:cmd:method:CompletePart.MarshalMsg", - "cmd:cmd:method:CompletePart.Msgsize", - "cmd:cmd:method:CompletePart.UnmarshalMsg", - "cmd:cmd:method:ConfigDir.Get", - "cmd:cmd:method:ConfigSys.Init", - "cmd:cmd:method:DailyAllTierStats.DecodeMsg", - "cmd:cmd:method:DailyAllTierStats.EncodeMsg", - "cmd:cmd:method:DailyAllTierStats.MarshalMsg", - "cmd:cmd:method:DailyAllTierStats.Msgsize", - "cmd:cmd:method:DailyAllTierStats.UnmarshalMsg", - "cmd:cmd:method:DataMovementOverwriteErr.Error", - "cmd:cmd:method:DecryptBlocksReader.Read", - "cmd:cmd:method:DeleteBulkReq.DecodeMsg", - "cmd:cmd:method:DeleteBulkReq.EncodeMsg", - "cmd:cmd:method:DeleteBulkReq.MarshalMsg", - "cmd:cmd:method:DeleteBulkReq.Msgsize", - "cmd:cmd:method:DeleteBulkReq.UnmarshalMsg", - "cmd:cmd:method:DeleteFileHandlerParams.DecodeMsg", - "cmd:cmd:method:DeleteFileHandlerParams.EncodeMsg", - "cmd:cmd:method:DeleteFileHandlerParams.MarshalMsg", - "cmd:cmd:method:DeleteFileHandlerParams.Msgsize", - "cmd:cmd:method:DeleteFileHandlerParams.UnmarshalMsg", - "cmd:cmd:method:DeleteMarkerMTime.MarshalXML", - "cmd:cmd:method:DeleteOptions.DecodeMsg", - "cmd:cmd:method:DeleteOptions.EncodeMsg", - "cmd:cmd:method:DeleteOptions.MarshalMsg", - "cmd:cmd:method:DeleteOptions.Msgsize", - "cmd:cmd:method:DeleteOptions.UnmarshalMsg", - "cmd:cmd:method:DeleteVersionHandlerParams.DecodeMsg", - "cmd:cmd:method:DeleteVersionHandlerParams.EncodeMsg", - "cmd:cmd:method:DeleteVersionHandlerParams.MarshalMsg", - "cmd:cmd:method:DeleteVersionHandlerParams.Msgsize", - "cmd:cmd:method:DeleteVersionHandlerParams.UnmarshalMsg", - "cmd:cmd:method:DeleteVersionsErrsResp.DecodeMsg", - "cmd:cmd:method:DeleteVersionsErrsResp.EncodeMsg", - "cmd:cmd:method:DeleteVersionsErrsResp.MarshalMsg", - "cmd:cmd:method:DeleteVersionsErrsResp.Msgsize", - "cmd:cmd:method:DeleteVersionsErrsResp.UnmarshalMsg", - "cmd:cmd:method:DeletedObject.DeleteMarkerReplicationStatus", - "cmd:cmd:method:DeletedObject.VersionPurgeStatus", - "cmd:cmd:method:DeletedObjectInfo.MarshalMsg", - "cmd:cmd:method:DeletedObjectInfo.Msgsize", - "cmd:cmd:method:DeletedObjectInfo.UnmarshalMsg", - "cmd:cmd:method:DeletedObjectReplicationInfo.ToMRFEntry", - "cmd:cmd:method:DiskInfo.DecodeMsg", - "cmd:cmd:method:DiskInfo.EncodeMsg", - "cmd:cmd:method:DiskInfo.MarshalMsg", - "cmd:cmd:method:DiskInfo.Msgsize", - "cmd:cmd:method:DiskInfo.UnmarshalMsg", - "cmd:cmd:method:DiskInfoOptions.DecodeMsg", - "cmd:cmd:method:DiskInfoOptions.EncodeMsg", - "cmd:cmd:method:DiskInfoOptions.MarshalMsg", - "cmd:cmd:method:DiskInfoOptions.Msgsize", - "cmd:cmd:method:DiskInfoOptions.UnmarshalMsg", - "cmd:cmd:method:DiskMetrics.DecodeMsg", - "cmd:cmd:method:DiskMetrics.EncodeMsg", - "cmd:cmd:method:DiskMetrics.MarshalMsg", - "cmd:cmd:method:DiskMetrics.Msgsize", - "cmd:cmd:method:DiskMetrics.UnmarshalMsg", - "cmd:cmd:method:Endpoint.Equal", - "cmd:cmd:method:Endpoint.GridHost", - "cmd:cmd:method:Endpoint.HTTPS", - "cmd:cmd:method:Endpoint.SetDiskIndex", - "cmd:cmd:method:Endpoint.SetPoolIndex", - "cmd:cmd:method:Endpoint.SetSetIndex", - "cmd:cmd:method:Endpoint.String", - "cmd:cmd:method:Endpoint.Type", - "cmd:cmd:method:Endpoint.UpdateIsLocal", - "cmd:cmd:method:EndpointServerPools.Add", - "cmd:cmd:method:EndpointServerPools.ESCount", - "cmd:cmd:method:EndpointServerPools.FindGridHostsFromPeer", - "cmd:cmd:method:EndpointServerPools.FindGridHostsFromPeerPool", - "cmd:cmd:method:EndpointServerPools.FindGridHostsFromPeerStr", - "cmd:cmd:method:EndpointServerPools.FirstLocal", - "cmd:cmd:method:EndpointServerPools.GetLocalPoolIdx", - "cmd:cmd:method:EndpointServerPools.GetNodes", - "cmd:cmd:method:EndpointServerPools.GetPoolIdx", - "cmd:cmd:method:EndpointServerPools.GridHosts", - "cmd:cmd:method:EndpointServerPools.HTTPS", - "cmd:cmd:method:EndpointServerPools.Hostnames", - "cmd:cmd:method:EndpointServerPools.Legacy", - "cmd:cmd:method:EndpointServerPools.LocalDisksPaths", - "cmd:cmd:method:EndpointServerPools.Localhost", - "cmd:cmd:method:EndpointServerPools.NEndpoints", - "cmd:cmd:method:EndpointServerPools.NLocalDisksPathsPerPool", - "cmd:cmd:method:Endpoints.GetAllStrings", - "cmd:cmd:method:Endpoints.GetString", - "cmd:cmd:method:Endpoints.HTTPS", - "cmd:cmd:method:Endpoints.UpdateIsLocal", - "cmd:cmd:method:Erasure.Decode", - "cmd:cmd:method:Erasure.DecodeDataAndParityBlocks", - "cmd:cmd:method:Erasure.DecodeDataBlocks", - "cmd:cmd:method:Erasure.Encode", - "cmd:cmd:method:Erasure.EncodeData", - "cmd:cmd:method:Erasure.Heal", - "cmd:cmd:method:Erasure.ShardFileOffset", - "cmd:cmd:method:Erasure.ShardFileSize", - "cmd:cmd:method:Erasure.ShardSize", - "cmd:cmd:method:ErasureAlgo.DecodeMsg", - "cmd:cmd:method:ErasureAlgo.EncodeMsg", - "cmd:cmd:method:ErasureAlgo.MarshalMsg", - "cmd:cmd:method:ErasureAlgo.Msgsize", - "cmd:cmd:method:ErasureAlgo.String", - "cmd:cmd:method:ErasureAlgo.UnmarshalMsg", - "cmd:cmd:method:ErasureInfo.DecodeMsg", - "cmd:cmd:method:ErasureInfo.EncodeMsg", - "cmd:cmd:method:ErasureInfo.Equal", - "cmd:cmd:method:ErasureInfo.GetChecksumInfo", - "cmd:cmd:method:ErasureInfo.MarshalMsg", - "cmd:cmd:method:ErasureInfo.Msgsize", - "cmd:cmd:method:ErasureInfo.ShardFileSize", - "cmd:cmd:method:ErasureInfo.ShardSize", - "cmd:cmd:method:ErasureInfo.UnmarshalMsg", - "cmd:cmd:method:EventNotifier.AddRulesMap", - "cmd:cmd:method:EventNotifier.GetARNList", - "cmd:cmd:method:EventNotifier.InitBucketTargets", - "cmd:cmd:method:EventNotifier.RemoveAllBucketTargets", - "cmd:cmd:method:EventNotifier.RemoveNotification", - "cmd:cmd:method:EventNotifier.Send", - "cmd:cmd:method:EventNotifier.Targets", - "cmd:cmd:method:ExpirationOptions.MarshalMsg", - "cmd:cmd:method:ExpirationOptions.Msgsize", - "cmd:cmd:method:ExpirationOptions.UnmarshalMsg", - "cmd:cmd:method:FileInfo.AddObjectPart", - "cmd:cmd:method:FileInfo.DataMov", - "cmd:cmd:method:FileInfo.DecodeMsg", - "cmd:cmd:method:FileInfo.DeleteMarkerReplicationStatus", - "cmd:cmd:method:FileInfo.EncodeMsg", - "cmd:cmd:method:FileInfo.Equals", - "cmd:cmd:method:FileInfo.GetDataDir", - "cmd:cmd:method:FileInfo.HasNegativePartSize", - "cmd:cmd:method:FileInfo.Healing", - "cmd:cmd:method:FileInfo.InlineData", - "cmd:cmd:method:FileInfo.IsCompressed", - "cmd:cmd:method:FileInfo.IsRemote", - "cmd:cmd:method:FileInfo.IsRestoreObjReq", - "cmd:cmd:method:FileInfo.IsValid", - "cmd:cmd:method:FileInfo.MarshalMsg", - "cmd:cmd:method:FileInfo.MetadataEquals", - "cmd:cmd:method:FileInfo.Msgsize", - "cmd:cmd:method:FileInfo.ObjectToPartOffset", - "cmd:cmd:method:FileInfo.ReadQuorum", - "cmd:cmd:method:FileInfo.ReplicationInfoEquals", - "cmd:cmd:method:FileInfo.ReplicationStatus", - "cmd:cmd:method:FileInfo.SetDataMov", - "cmd:cmd:method:FileInfo.SetHealing", - "cmd:cmd:method:FileInfo.SetInlineData", - "cmd:cmd:method:FileInfo.SetSkipTierFreeVersion", - "cmd:cmd:method:FileInfo.SetTierFreeVersion", - "cmd:cmd:method:FileInfo.SetTierFreeVersionID", - "cmd:cmd:method:FileInfo.ShallowCopy", - "cmd:cmd:method:FileInfo.ShardFileSize", - "cmd:cmd:method:FileInfo.SkipTierFreeVersion", - "cmd:cmd:method:FileInfo.TierFreeVersion", - "cmd:cmd:method:FileInfo.TierFreeVersionID", - "cmd:cmd:method:FileInfo.ToObjectInfo", - "cmd:cmd:method:FileInfo.TransitionInfoEquals", - "cmd:cmd:method:FileInfo.UnmarshalMsg", - "cmd:cmd:method:FileInfo.VersionPurgeStatus", - "cmd:cmd:method:FileInfo.WriteQuorum", - "cmd:cmd:method:FileInfoVersions.DecodeMsg", - "cmd:cmd:method:FileInfoVersions.EncodeMsg", - "cmd:cmd:method:FileInfoVersions.MarshalMsg", - "cmd:cmd:method:FileInfoVersions.Msgsize", - "cmd:cmd:method:FileInfoVersions.Size", - "cmd:cmd:method:FileInfoVersions.UnmarshalMsg", - "cmd:cmd:method:FilesInfo.DecodeMsg", - "cmd:cmd:method:FilesInfo.EncodeMsg", - "cmd:cmd:method:FilesInfo.MarshalMsg", - "cmd:cmd:method:FilesInfo.Msgsize", - "cmd:cmd:method:FilesInfo.UnmarshalMsg", - "cmd:cmd:method:GenericError.Unwrap", - "cmd:cmd:method:GetObjectReader.Close", - "cmd:cmd:method:GetObjectReader.WithCleanupFuncs", - "cmd:cmd:method:HTTPAPIStats.Dec", - "cmd:cmd:method:HTTPAPIStats.Get", - "cmd:cmd:method:HTTPAPIStats.Inc", - "cmd:cmd:method:HTTPAPIStats.Load", - "cmd:cmd:method:HTTPConsoleLoggerSys.Cancel", - "cmd:cmd:method:HTTPConsoleLoggerSys.Content", - "cmd:cmd:method:HTTPConsoleLoggerSys.Endpoint", - "cmd:cmd:method:HTTPConsoleLoggerSys.HasLogListeners", - "cmd:cmd:method:HTTPConsoleLoggerSys.Init", - "cmd:cmd:method:HTTPConsoleLoggerSys.IsOnline", - "cmd:cmd:method:HTTPConsoleLoggerSys.Send", - "cmd:cmd:method:HTTPConsoleLoggerSys.SetNodeName", - "cmd:cmd:method:HTTPConsoleLoggerSys.Stats", - "cmd:cmd:method:HTTPConsoleLoggerSys.String", - "cmd:cmd:method:HTTPConsoleLoggerSys.Subscribe", - "cmd:cmd:method:HTTPConsoleLoggerSys.Type", - "cmd:cmd:method:HTTPRangeSpec.GetLength", - "cmd:cmd:method:HTTPRangeSpec.GetOffsetLength", - "cmd:cmd:method:HTTPRangeSpec.String", - "cmd:cmd:method:HTTPRangeSpec.ToHeader", - "cmd:cmd:method:HealthResult.String", - "cmd:cmd:method:IAMStoreSys.AddServiceAccount", - "cmd:cmd:method:IAMStoreSys.AddUser", - "cmd:cmd:method:IAMStoreSys.AddUsersToGroup", - "cmd:cmd:method:IAMStoreSys.DeletePolicy", - "cmd:cmd:method:IAMStoreSys.DeleteUser", - "cmd:cmd:method:IAMStoreSys.DeleteUsers", - "cmd:cmd:method:IAMStoreSys.GetAllParentUsers", - "cmd:cmd:method:IAMStoreSys.GetAllSTSUserMappings", - "cmd:cmd:method:IAMStoreSys.GetBucketUsers", - "cmd:cmd:method:IAMStoreSys.GetGroupDescription", - "cmd:cmd:method:IAMStoreSys.GetMappedPolicy", - "cmd:cmd:method:IAMStoreSys.GetPolicy", - "cmd:cmd:method:IAMStoreSys.GetPolicyDoc", - "cmd:cmd:method:IAMStoreSys.GetSTSAndServiceAccounts", - "cmd:cmd:method:IAMStoreSys.GetUser", - "cmd:cmd:method:IAMStoreSys.GetUserInfo", - "cmd:cmd:method:IAMStoreSys.GetUsers", - "cmd:cmd:method:IAMStoreSys.GetUsersWithMappedPolicies", - "cmd:cmd:method:IAMStoreSys.GroupNotificationHandler", - "cmd:cmd:method:IAMStoreSys.HasWatcher", - "cmd:cmd:method:IAMStoreSys.ListAccessKeys", - "cmd:cmd:method:IAMStoreSys.ListGroups", - "cmd:cmd:method:IAMStoreSys.ListPolicies", - "cmd:cmd:method:IAMStoreSys.ListPolicyDocs", - "cmd:cmd:method:IAMStoreSys.ListPolicyMappings", - "cmd:cmd:method:IAMStoreSys.ListSTSAccounts", - "cmd:cmd:method:IAMStoreSys.ListServiceAccounts", - "cmd:cmd:method:IAMStoreSys.ListTempAccounts", - "cmd:cmd:method:IAMStoreSys.LoadIAMCache", - "cmd:cmd:method:IAMStoreSys.LoadUser", - "cmd:cmd:method:IAMStoreSys.MergePolicies", - "cmd:cmd:method:IAMStoreSys.PolicyDBGet", - "cmd:cmd:method:IAMStoreSys.PolicyDBSet", - "cmd:cmd:method:IAMStoreSys.PolicyDBUpdate", - "cmd:cmd:method:IAMStoreSys.PolicyMappingNotificationHandler", - "cmd:cmd:method:IAMStoreSys.PolicyNotificationHandler", - "cmd:cmd:method:IAMStoreSys.RemoveUsersFromGroup", - "cmd:cmd:method:IAMStoreSys.RevokeTokens", - "cmd:cmd:method:IAMStoreSys.SetGroupStatus", - "cmd:cmd:method:IAMStoreSys.SetPolicy", - "cmd:cmd:method:IAMStoreSys.SetTempUser", - "cmd:cmd:method:IAMStoreSys.SetUserStatus", - "cmd:cmd:method:IAMStoreSys.UpdateServiceAccount", - "cmd:cmd:method:IAMStoreSys.UpdateUserIdentity", - "cmd:cmd:method:IAMStoreSys.UpdateUserSecretKey", - "cmd:cmd:method:IAMStoreSys.UserNotificationHandler", - "cmd:cmd:method:IAMSys.AddUsersToGroup", - "cmd:cmd:method:IAMSys.CheckKey", - "cmd:cmd:method:IAMSys.CreateUser", - "cmd:cmd:method:IAMSys.CurrentPolicies", - "cmd:cmd:method:IAMSys.DeletePolicy", - "cmd:cmd:method:IAMSys.DeleteServiceAccount", - "cmd:cmd:method:IAMSys.DeleteUser", - "cmd:cmd:method:IAMSys.GetClaimsForSvcAcc", - "cmd:cmd:method:IAMSys.GetCombinedPolicy", - "cmd:cmd:method:IAMSys.GetGroupDescription", - "cmd:cmd:method:IAMSys.GetRolePolicy", - "cmd:cmd:method:IAMSys.GetServiceAccount", - "cmd:cmd:method:IAMSys.GetTemporaryAccount", - "cmd:cmd:method:IAMSys.GetUser", - "cmd:cmd:method:IAMSys.GetUserInfo", - "cmd:cmd:method:IAMSys.GetUsersSysType", - "cmd:cmd:method:IAMSys.HasRolePolicy", - "cmd:cmd:method:IAMSys.HasWatcher", - "cmd:cmd:method:IAMSys.InfoPolicy", - "cmd:cmd:method:IAMSys.Init", - "cmd:cmd:method:IAMSys.Initialized", - "cmd:cmd:method:IAMSys.IsAllowed", - "cmd:cmd:method:IAMSys.IsAllowedSTS", - "cmd:cmd:method:IAMSys.IsAllowedServiceAccount", - "cmd:cmd:method:IAMSys.IsServiceAccount", - "cmd:cmd:method:IAMSys.IsTempUser", - "cmd:cmd:method:IAMSys.ListAllAccessKeys", - "cmd:cmd:method:IAMSys.ListBucketUsers", - "cmd:cmd:method:IAMSys.ListGroups", - "cmd:cmd:method:IAMSys.ListLDAPUsers", - "cmd:cmd:method:IAMSys.ListPolicies", - "cmd:cmd:method:IAMSys.ListPolicyDocs", - "cmd:cmd:method:IAMSys.ListSTSAccounts", - "cmd:cmd:method:IAMSys.ListServiceAccounts", - "cmd:cmd:method:IAMSys.ListTempAccounts", - "cmd:cmd:method:IAMSys.ListUsers", - "cmd:cmd:method:IAMSys.Load", - "cmd:cmd:method:IAMSys.LoadGroup", - "cmd:cmd:method:IAMSys.LoadPolicy", - "cmd:cmd:method:IAMSys.LoadPolicyMapping", - "cmd:cmd:method:IAMSys.LoadServiceAccount", - "cmd:cmd:method:IAMSys.LoadUser", - "cmd:cmd:method:IAMSys.NewServiceAccount", - "cmd:cmd:method:IAMSys.NormalizeLDAPAccessKeypairs", - "cmd:cmd:method:IAMSys.NormalizeLDAPMappingImport", - "cmd:cmd:method:IAMSys.PolicyDBGet", - "cmd:cmd:method:IAMSys.PolicyDBSet", - "cmd:cmd:method:IAMSys.PolicyDBUpdateBuiltin", - "cmd:cmd:method:IAMSys.PolicyDBUpdateLDAP", - "cmd:cmd:method:IAMSys.QueryLDAPPolicyEntities", - "cmd:cmd:method:IAMSys.QueryPolicyEntities", - "cmd:cmd:method:IAMSys.RemoveUsersFromGroup", - "cmd:cmd:method:IAMSys.RevokeTokens", - "cmd:cmd:method:IAMSys.SetGroupStatus", - "cmd:cmd:method:IAMSys.SetPolicy", - "cmd:cmd:method:IAMSys.SetTempUser", - "cmd:cmd:method:IAMSys.SetUserSecretKey", - "cmd:cmd:method:IAMSys.SetUserStatus", - "cmd:cmd:method:IAMSys.SetUsersSysType", - "cmd:cmd:method:IAMSys.UpdateServiceAccount", - "cmd:cmd:method:InQueueMetric.DecodeMsg", - "cmd:cmd:method:InQueueMetric.EncodeMsg", - "cmd:cmd:method:InQueueMetric.MarshalMsg", - "cmd:cmd:method:InQueueMetric.Msgsize", - "cmd:cmd:method:InQueueMetric.UnmarshalMsg", - "cmd:cmd:method:InQueueStats.DecodeMsg", - "cmd:cmd:method:InQueueStats.EncodeMsg", - "cmd:cmd:method:InQueueStats.MarshalMsg", - "cmd:cmd:method:InQueueStats.Msgsize", - "cmd:cmd:method:InQueueStats.UnmarshalMsg", - "cmd:cmd:method:IncompleteBody.Error", - "cmd:cmd:method:InsufficientReadQuorum.Error", - "cmd:cmd:method:InsufficientReadQuorum.Unwrap", - "cmd:cmd:method:InsufficientWriteQuorum.Error", - "cmd:cmd:method:InsufficientWriteQuorum.Unwrap", - "cmd:cmd:method:InvalidArgument.Error", - "cmd:cmd:method:InvalidETag.Error", - "cmd:cmd:method:InvalidObjectState.Error", - "cmd:cmd:method:InvalidPart.Error", - "cmd:cmd:method:InvalidRange.Error", - "cmd:cmd:method:InvalidUploadID.Error", - "cmd:cmd:method:InvalidUploadIDKeyCombination.Error", - "cmd:cmd:method:InvalidVersionID.Error", - "cmd:cmd:method:KMSLogger.LogIf", - "cmd:cmd:method:KMSLogger.LogOnceIf", - "cmd:cmd:method:LastMinuteHistogram.Add", - "cmd:cmd:method:LastMinuteHistogram.DecodeMsg", - "cmd:cmd:method:LastMinuteHistogram.EncodeMsg", - "cmd:cmd:method:LastMinuteHistogram.GetAvgData", - "cmd:cmd:method:LastMinuteHistogram.MarshalMsg", - "cmd:cmd:method:LastMinuteHistogram.Merge", - "cmd:cmd:method:LastMinuteHistogram.Msgsize", - "cmd:cmd:method:LastMinuteHistogram.UnmarshalMsg", - "cmd:cmd:method:LifecycleSys.Get", - "cmd:cmd:method:ListDirResult.DecodeMsg", - "cmd:cmd:method:ListDirResult.EncodeMsg", - "cmd:cmd:method:ListDirResult.MarshalMsg", - "cmd:cmd:method:ListDirResult.Msgsize", - "cmd:cmd:method:ListDirResult.UnmarshalMsg", - "cmd:cmd:method:ListMultipartsInfo.Lookup", - "cmd:cmd:method:ListMultipartsInfo.MarshalMsg", - "cmd:cmd:method:ListMultipartsInfo.Msgsize", - "cmd:cmd:method:ListMultipartsInfo.UnmarshalMsg", - "cmd:cmd:method:ListObjectVersionsInfo.MarshalMsg", - "cmd:cmd:method:ListObjectVersionsInfo.Msgsize", - "cmd:cmd:method:ListObjectVersionsInfo.UnmarshalMsg", - "cmd:cmd:method:ListObjectsInfo.MarshalMsg", - "cmd:cmd:method:ListObjectsInfo.Msgsize", - "cmd:cmd:method:ListObjectsInfo.UnmarshalMsg", - "cmd:cmd:method:ListObjectsV2Info.MarshalMsg", - "cmd:cmd:method:ListObjectsV2Info.Msgsize", - "cmd:cmd:method:ListObjectsV2Info.UnmarshalMsg", - "cmd:cmd:method:ListPartsInfo.MarshalMsg", - "cmd:cmd:method:ListPartsInfo.Msgsize", - "cmd:cmd:method:ListPartsInfo.UnmarshalMsg", - "cmd:cmd:method:LocalDiskIDs.DecodeMsg", - "cmd:cmd:method:LocalDiskIDs.EncodeMsg", - "cmd:cmd:method:LocalDiskIDs.MarshalMsg", - "cmd:cmd:method:LocalDiskIDs.Msgsize", - "cmd:cmd:method:LocalDiskIDs.UnmarshalMsg", - "cmd:cmd:method:LockContext.Cancel", - "cmd:cmd:method:LockContext.Context", - "cmd:cmd:method:MRFReplicateEntries.DecodeMsg", - "cmd:cmd:method:MRFReplicateEntries.EncodeMsg", - "cmd:cmd:method:MRFReplicateEntries.MarshalMsg", - "cmd:cmd:method:MRFReplicateEntries.Msgsize", - "cmd:cmd:method:MRFReplicateEntries.UnmarshalMsg", - "cmd:cmd:method:MRFReplicateEntry.DecodeMsg", - "cmd:cmd:method:MRFReplicateEntry.EncodeMsg", - "cmd:cmd:method:MRFReplicateEntry.MarshalMsg", - "cmd:cmd:method:MRFReplicateEntry.Msgsize", - "cmd:cmd:method:MRFReplicateEntry.UnmarshalMsg", - "cmd:cmd:method:MakeBucketOptions.MarshalMsg", - "cmd:cmd:method:MakeBucketOptions.Msgsize", - "cmd:cmd:method:MakeBucketOptions.UnmarshalMsg", - "cmd:cmd:method:MalformedUploadID.Error", - "cmd:cmd:method:Metadata.MarshalXML", - "cmd:cmd:method:Metadata.Set", - "cmd:cmd:method:MetadataHandlerParams.DecodeMsg", - "cmd:cmd:method:MetadataHandlerParams.EncodeMsg", - "cmd:cmd:method:MetadataHandlerParams.MarshalMsg", - "cmd:cmd:method:MetadataHandlerParams.Msgsize", - "cmd:cmd:method:MetadataHandlerParams.UnmarshalMsg", - "cmd:cmd:method:MethodNotAllowed.Error", - "cmd:cmd:method:MetricDescription.MarshalMsg", - "cmd:cmd:method:MetricDescription.Msgsize", - "cmd:cmd:method:MetricDescription.UnmarshalMsg", - "cmd:cmd:method:MetricName.MarshalMsg", - "cmd:cmd:method:MetricName.Msgsize", - "cmd:cmd:method:MetricName.UnmarshalMsg", - "cmd:cmd:method:MetricNamespace.MarshalMsg", - "cmd:cmd:method:MetricNamespace.Msgsize", - "cmd:cmd:method:MetricNamespace.UnmarshalMsg", - "cmd:cmd:method:MetricSubsystem.MarshalMsg", - "cmd:cmd:method:MetricSubsystem.Msgsize", - "cmd:cmd:method:MetricSubsystem.UnmarshalMsg", - "cmd:cmd:method:MetricType.String", - "cmd:cmd:method:MetricTypeV2.MarshalMsg", - "cmd:cmd:method:MetricTypeV2.Msgsize", - "cmd:cmd:method:MetricTypeV2.UnmarshalMsg", - "cmd:cmd:method:MetricV2.MarshalMsg", - "cmd:cmd:method:MetricV2.Msgsize", - "cmd:cmd:method:MetricV2.UnmarshalMsg", - "cmd:cmd:method:MetricValues.Set", - "cmd:cmd:method:MetricValues.SetHistogram", - "cmd:cmd:method:MetricValues.ToPromMetrics", - "cmd:cmd:method:MetricsGroup.AddExtraLabels", - "cmd:cmd:method:MetricsGroup.Collect", - "cmd:cmd:method:MetricsGroup.Describe", - "cmd:cmd:method:MetricsGroup.IsBucketMetricsGroup", - "cmd:cmd:method:MetricsGroup.LockAndSetBuckets", - "cmd:cmd:method:MetricsGroup.MetricFQN", - "cmd:cmd:method:MetricsGroup.SetCache", - "cmd:cmd:method:MetricsGroupOpts.MarshalMsg", - "cmd:cmd:method:MetricsGroupOpts.Msgsize", - "cmd:cmd:method:MetricsGroupOpts.UnmarshalMsg", - "cmd:cmd:method:MetricsGroupV2.Get", - "cmd:cmd:method:MetricsGroupV2.MarshalMsg", - "cmd:cmd:method:MetricsGroupV2.Msgsize", - "cmd:cmd:method:MetricsGroupV2.RegisterRead", - "cmd:cmd:method:MetricsGroupV2.UnmarshalMsg", - "cmd:cmd:method:MultipartInfo.KMSKeyID", - "cmd:cmd:method:MultipartInfo.MarshalMsg", - "cmd:cmd:method:MultipartInfo.Msgsize", - "cmd:cmd:method:MultipartInfo.UnmarshalMsg", - "cmd:cmd:method:NewMultipartUploadResult.MarshalMsg", - "cmd:cmd:method:NewMultipartUploadResult.Msgsize", - "cmd:cmd:method:NewMultipartUploadResult.UnmarshalMsg", - "cmd:cmd:method:NotImplemented.Error", - "cmd:cmd:method:NotificationGroup.Go", - "cmd:cmd:method:NotificationGroup.Wait", - "cmd:cmd:method:NotificationGroup.WithRetries", - "cmd:cmd:method:NotificationSys.BackgroundHealStatus", - "cmd:cmd:method:NotificationSys.CommitBinary", - "cmd:cmd:method:NotificationSys.DeleteBucketMetadata", - "cmd:cmd:method:NotificationSys.DeletePolicy", - "cmd:cmd:method:NotificationSys.DeleteServiceAccount", - "cmd:cmd:method:NotificationSys.DeleteUploadID", - "cmd:cmd:method:NotificationSys.DeleteUser", - "cmd:cmd:method:NotificationSys.DownloadProfilingData", - "cmd:cmd:method:NotificationSys.DriveSpeedTest", - "cmd:cmd:method:NotificationSys.GetBandwidthReports", - "cmd:cmd:method:NotificationSys.GetBucketMetrics", - "cmd:cmd:method:NotificationSys.GetCPUs", - "cmd:cmd:method:NotificationSys.GetClusterAllBucketStats", - "cmd:cmd:method:NotificationSys.GetClusterBucketStats", - "cmd:cmd:method:NotificationSys.GetClusterMetrics", - "cmd:cmd:method:NotificationSys.GetClusterSiteMetrics", - "cmd:cmd:method:NotificationSys.GetLastDayTierStats", - "cmd:cmd:method:NotificationSys.GetLocks", - "cmd:cmd:method:NotificationSys.GetMemInfo", - "cmd:cmd:method:NotificationSys.GetMetrics", - "cmd:cmd:method:NotificationSys.GetNetInfo", - "cmd:cmd:method:NotificationSys.GetOSInfo", - "cmd:cmd:method:NotificationSys.GetPartitions", - "cmd:cmd:method:NotificationSys.GetPeerOnlineCount", - "cmd:cmd:method:NotificationSys.GetProcInfo", - "cmd:cmd:method:NotificationSys.GetReplicationMRF", - "cmd:cmd:method:NotificationSys.GetResourceMetrics", - "cmd:cmd:method:NotificationSys.GetSysConfig", - "cmd:cmd:method:NotificationSys.GetSysErrors", - "cmd:cmd:method:NotificationSys.GetSysServices", - "cmd:cmd:method:NotificationSys.LoadBucketMetadata", - "cmd:cmd:method:NotificationSys.LoadGroup", - "cmd:cmd:method:NotificationSys.LoadPolicy", - "cmd:cmd:method:NotificationSys.LoadPolicyMapping", - "cmd:cmd:method:NotificationSys.LoadRebalanceMeta", - "cmd:cmd:method:NotificationSys.LoadServiceAccount", - "cmd:cmd:method:NotificationSys.LoadTransitionTierConfig", - "cmd:cmd:method:NotificationSys.LoadUser", - "cmd:cmd:method:NotificationSys.Netperf", - "cmd:cmd:method:NotificationSys.ReloadPoolMeta", - "cmd:cmd:method:NotificationSys.ReloadSiteReplicationConfig", - "cmd:cmd:method:NotificationSys.ServerInfo", - "cmd:cmd:method:NotificationSys.ServiceFreeze", - "cmd:cmd:method:NotificationSys.SignalConfigReload", - "cmd:cmd:method:NotificationSys.SignalService", - "cmd:cmd:method:NotificationSys.SignalServiceV2", - "cmd:cmd:method:NotificationSys.SpeedTest", - "cmd:cmd:method:NotificationSys.StartProfiling", - "cmd:cmd:method:NotificationSys.StopRebalance", - "cmd:cmd:method:NotificationSys.StorageInfo", - "cmd:cmd:method:NotificationSys.VerifyBinary", - "cmd:cmd:method:ObjectAlreadyExists.Error", - "cmd:cmd:method:ObjectExistsAsDirectory.Error", - "cmd:cmd:method:ObjectInfo.ArchiveInfo", - "cmd:cmd:method:ObjectInfo.Clone", - "cmd:cmd:method:ObjectInfo.DecryptedSize", - "cmd:cmd:method:ObjectInfo.EncryptedSize", - "cmd:cmd:method:ObjectInfo.ExpiresStr", - "cmd:cmd:method:ObjectInfo.GetActualSize", - "cmd:cmd:method:ObjectInfo.GetDecryptedRange", - "cmd:cmd:method:ObjectInfo.IsCompressed", - "cmd:cmd:method:ObjectInfo.IsCompressedOK", - "cmd:cmd:method:ObjectInfo.IsRemote", - "cmd:cmd:method:ObjectInfo.KMSKeyID", - "cmd:cmd:method:ObjectInfo.MarshalMsg", - "cmd:cmd:method:ObjectInfo.Msgsize", - "cmd:cmd:method:ObjectInfo.ReplicationState", - "cmd:cmd:method:ObjectInfo.TargetReplicationStatus", - "cmd:cmd:method:ObjectInfo.ToLifecycleOpts", - "cmd:cmd:method:ObjectInfo.TraceObjName", - "cmd:cmd:method:ObjectInfo.TraceVersionID", - "cmd:cmd:method:ObjectInfo.UnmarshalMsg", - "cmd:cmd:method:ObjectLocked.Error", - "cmd:cmd:method:ObjectNameInvalid.Error", - "cmd:cmd:method:ObjectNamePrefixAsSlash.Error", - "cmd:cmd:method:ObjectNameTooLong.Error", - "cmd:cmd:method:ObjectNotFound.Error", - "cmd:cmd:method:ObjectOptions.DeleteMarkerReplicationStatus", - "cmd:cmd:method:ObjectOptions.PutReplicationState", - "cmd:cmd:method:ObjectOptions.SetDeleteReplicationState", - "cmd:cmd:method:ObjectOptions.SetEvalMetadataFn", - "cmd:cmd:method:ObjectOptions.SetEvalRetentionBypassFn", - "cmd:cmd:method:ObjectOptions.SetReplicaStatus", - "cmd:cmd:method:ObjectOptions.VersionPurgeStatus", - "cmd:cmd:method:ObjectPartInfo.DecodeMsg", - "cmd:cmd:method:ObjectPartInfo.EncodeMsg", - "cmd:cmd:method:ObjectPartInfo.MarshalMsg", - "cmd:cmd:method:ObjectPartInfo.Msgsize", - "cmd:cmd:method:ObjectPartInfo.UnmarshalMsg", - "cmd:cmd:method:ObjectToDelete.ReplicationState", - "cmd:cmd:method:ObjectToDelete.TraceObjName", - "cmd:cmd:method:ObjectToDelete.TraceVersionID", - "cmd:cmd:method:ObjectTooLarge.Error", - "cmd:cmd:method:ObjectTooSmall.Error", - "cmd:cmd:method:ObjectVersion.MarshalXML", - "cmd:cmd:method:OperationTimedOut.Error", - "cmd:cmd:method:OutputLocation.IsEmpty", - "cmd:cmd:method:PartInfo.MarshalMsg", - "cmd:cmd:method:PartInfo.Msgsize", - "cmd:cmd:method:PartInfo.UnmarshalMsg", - "cmd:cmd:method:PartTooBig.Error", - "cmd:cmd:method:PartTooSmall.Error", - "cmd:cmd:method:PartialOperation.DecodeMsg", - "cmd:cmd:method:PartialOperation.EncodeMsg", - "cmd:cmd:method:PartialOperation.MarshalMsg", - "cmd:cmd:method:PartialOperation.Msgsize", - "cmd:cmd:method:PartialOperation.UnmarshalMsg", - "cmd:cmd:method:PolicySys.Get", - "cmd:cmd:method:PolicySys.IsAllowed", - "cmd:cmd:method:PoolDecommissionInfo.Clone", - "cmd:cmd:method:PoolDecommissionInfo.DecodeMsg", - "cmd:cmd:method:PoolDecommissionInfo.EncodeMsg", - "cmd:cmd:method:PoolDecommissionInfo.MarshalMsg", - "cmd:cmd:method:PoolDecommissionInfo.Msgsize", - "cmd:cmd:method:PoolDecommissionInfo.UnmarshalMsg", - "cmd:cmd:method:PoolEndpointList.UpdateIsLocal", - "cmd:cmd:method:PoolStatus.Clone", - "cmd:cmd:method:PoolStatus.DecodeMsg", - "cmd:cmd:method:PoolStatus.EncodeMsg", - "cmd:cmd:method:PoolStatus.MarshalMsg", - "cmd:cmd:method:PoolStatus.Msgsize", - "cmd:cmd:method:PoolStatus.UnmarshalMsg", - "cmd:cmd:method:PreConditionFailed.Error", - "cmd:cmd:method:PrefixAccessDenied.Error", - "cmd:cmd:method:ProxyMetric.DecodeMsg", - "cmd:cmd:method:ProxyMetric.EncodeMsg", - "cmd:cmd:method:ProxyMetric.MarshalMsg", - "cmd:cmd:method:ProxyMetric.Msgsize", - "cmd:cmd:method:ProxyMetric.UnmarshalMsg", - "cmd:cmd:method:PutObjReader.MD5CurrentHexString", - "cmd:cmd:method:PutObjReader.RawServerSideChecksumResult", - "cmd:cmd:method:PutObjReader.Size", - "cmd:cmd:method:PutObjReader.WithEncryption", - "cmd:cmd:method:QStat.DecodeMsg", - "cmd:cmd:method:QStat.EncodeMsg", - "cmd:cmd:method:QStat.MarshalMsg", - "cmd:cmd:method:QStat.Msgsize", - "cmd:cmd:method:QStat.UnmarshalMsg", - "cmd:cmd:method:RMetricName.DecodeMsg", - "cmd:cmd:method:RMetricName.EncodeMsg", - "cmd:cmd:method:RMetricName.MarshalMsg", - "cmd:cmd:method:RMetricName.Msgsize", - "cmd:cmd:method:RMetricName.UnmarshalMsg", - "cmd:cmd:method:RQErrType.String", - "cmd:cmd:method:RStat.DecodeMsg", - "cmd:cmd:method:RStat.EncodeMsg", - "cmd:cmd:method:RStat.MarshalMsg", - "cmd:cmd:method:RStat.Msgsize", - "cmd:cmd:method:RStat.UnmarshalMsg", - "cmd:cmd:method:RTimedMetrics.DecodeMsg", - "cmd:cmd:method:RTimedMetrics.EncodeMsg", - "cmd:cmd:method:RTimedMetrics.MarshalMsg", - "cmd:cmd:method:RTimedMetrics.Msgsize", - "cmd:cmd:method:RTimedMetrics.String", - "cmd:cmd:method:RTimedMetrics.UnmarshalMsg", - "cmd:cmd:method:RawFileInfo.DecodeMsg", - "cmd:cmd:method:RawFileInfo.EncodeMsg", - "cmd:cmd:method:RawFileInfo.MarshalMsg", - "cmd:cmd:method:RawFileInfo.Msgsize", - "cmd:cmd:method:RawFileInfo.UnmarshalMsg", - "cmd:cmd:method:ReadAllHandlerParams.DecodeMsg", - "cmd:cmd:method:ReadAllHandlerParams.EncodeMsg", - "cmd:cmd:method:ReadAllHandlerParams.MarshalMsg", - "cmd:cmd:method:ReadAllHandlerParams.Msgsize", - "cmd:cmd:method:ReadAllHandlerParams.UnmarshalMsg", - "cmd:cmd:method:ReadPartsReq.DecodeMsg", - "cmd:cmd:method:ReadPartsReq.EncodeMsg", - "cmd:cmd:method:ReadPartsReq.MarshalMsg", - "cmd:cmd:method:ReadPartsReq.Msgsize", - "cmd:cmd:method:ReadPartsReq.UnmarshalMsg", - "cmd:cmd:method:ReadPartsResp.DecodeMsg", - "cmd:cmd:method:ReadPartsResp.EncodeMsg", - "cmd:cmd:method:ReadPartsResp.MarshalMsg", - "cmd:cmd:method:ReadPartsResp.Msgsize", - "cmd:cmd:method:ReadPartsResp.UnmarshalMsg", - "cmd:cmd:method:RemoteTargetConnectionErr.Error", - "cmd:cmd:method:RenameDataHandlerParams.DecodeMsg", - "cmd:cmd:method:RenameDataHandlerParams.EncodeMsg", - "cmd:cmd:method:RenameDataHandlerParams.MarshalMsg", - "cmd:cmd:method:RenameDataHandlerParams.Msgsize", - "cmd:cmd:method:RenameDataHandlerParams.UnmarshalMsg", - "cmd:cmd:method:RenameDataInlineHandlerParams.DecodeMsg", - "cmd:cmd:method:RenameDataInlineHandlerParams.EncodeMsg", - "cmd:cmd:method:RenameDataInlineHandlerParams.MarshalMsg", - "cmd:cmd:method:RenameDataInlineHandlerParams.Msgsize", - "cmd:cmd:method:RenameDataInlineHandlerParams.Recycle", - "cmd:cmd:method:RenameDataInlineHandlerParams.UnmarshalMsg", - "cmd:cmd:method:RenameDataResp.DecodeMsg", - "cmd:cmd:method:RenameDataResp.EncodeMsg", - "cmd:cmd:method:RenameDataResp.MarshalMsg", - "cmd:cmd:method:RenameDataResp.Msgsize", - "cmd:cmd:method:RenameDataResp.UnmarshalMsg", - "cmd:cmd:method:RenameFileHandlerParams.DecodeMsg", - "cmd:cmd:method:RenameFileHandlerParams.EncodeMsg", - "cmd:cmd:method:RenameFileHandlerParams.MarshalMsg", - "cmd:cmd:method:RenameFileHandlerParams.Msgsize", - "cmd:cmd:method:RenameFileHandlerParams.UnmarshalMsg", - "cmd:cmd:method:RenameOptions.DecodeMsg", - "cmd:cmd:method:RenameOptions.EncodeMsg", - "cmd:cmd:method:RenameOptions.MarshalMsg", - "cmd:cmd:method:RenameOptions.Msgsize", - "cmd:cmd:method:RenameOptions.UnmarshalMsg", - "cmd:cmd:method:RenamePartHandlerParams.DecodeMsg", - "cmd:cmd:method:RenamePartHandlerParams.EncodeMsg", - "cmd:cmd:method:RenamePartHandlerParams.MarshalMsg", - "cmd:cmd:method:RenamePartHandlerParams.Msgsize", - "cmd:cmd:method:RenamePartHandlerParams.UnmarshalMsg", - "cmd:cmd:method:ReplQNodeStats.DecodeMsg", - "cmd:cmd:method:ReplQNodeStats.EncodeMsg", - "cmd:cmd:method:ReplQNodeStats.MarshalMsg", - "cmd:cmd:method:ReplQNodeStats.Msgsize", - "cmd:cmd:method:ReplQNodeStats.UnmarshalMsg", - "cmd:cmd:method:ReplicateDecision.DecodeMsg", - "cmd:cmd:method:ReplicateDecision.EncodeMsg", - "cmd:cmd:method:ReplicateDecision.MarshalMsg", - "cmd:cmd:method:ReplicateDecision.Msgsize", - "cmd:cmd:method:ReplicateDecision.PendingStatus", - "cmd:cmd:method:ReplicateDecision.ReplicateAny", - "cmd:cmd:method:ReplicateDecision.Set", - "cmd:cmd:method:ReplicateDecision.String", - "cmd:cmd:method:ReplicateDecision.Synchronous", - "cmd:cmd:method:ReplicateDecision.UnmarshalMsg", - "cmd:cmd:method:ReplicateObjectInfo.MarshalMsg", - "cmd:cmd:method:ReplicateObjectInfo.Msgsize", - "cmd:cmd:method:ReplicateObjectInfo.TargetReplicationStatus", - "cmd:cmd:method:ReplicateObjectInfo.ToMRFEntry", - "cmd:cmd:method:ReplicateObjectInfo.ToObjectInfo", - "cmd:cmd:method:ReplicateObjectInfo.UnmarshalMsg", - "cmd:cmd:method:ReplicationLastHour.DecodeMsg", - "cmd:cmd:method:ReplicationLastHour.EncodeMsg", - "cmd:cmd:method:ReplicationLastHour.MarshalMsg", - "cmd:cmd:method:ReplicationLastHour.Msgsize", - "cmd:cmd:method:ReplicationLastHour.UnmarshalMsg", - "cmd:cmd:method:ReplicationLastMinute.DecodeMsg", - "cmd:cmd:method:ReplicationLastMinute.EncodeMsg", - "cmd:cmd:method:ReplicationLastMinute.MarshalMsg", - "cmd:cmd:method:ReplicationLastMinute.Msgsize", - "cmd:cmd:method:ReplicationLastMinute.String", - "cmd:cmd:method:ReplicationLastMinute.UnmarshalMsg", - "cmd:cmd:method:ReplicationLatency.DecodeMsg", - "cmd:cmd:method:ReplicationLatency.EncodeMsg", - "cmd:cmd:method:ReplicationLatency.MarshalMsg", - "cmd:cmd:method:ReplicationLatency.Msgsize", - "cmd:cmd:method:ReplicationLatency.UnmarshalMsg", - "cmd:cmd:method:ReplicationMRFStats.DecodeMsg", - "cmd:cmd:method:ReplicationMRFStats.EncodeMsg", - "cmd:cmd:method:ReplicationMRFStats.MarshalMsg", - "cmd:cmd:method:ReplicationMRFStats.Msgsize", - "cmd:cmd:method:ReplicationMRFStats.UnmarshalMsg", - "cmd:cmd:method:ReplicationPermissionCheck.Error", - "cmd:cmd:method:ReplicationPool.ActiveLrgWorkers", - "cmd:cmd:method:ReplicationPool.ActiveMRFWorkers", - "cmd:cmd:method:ReplicationPool.ActiveWorkers", - "cmd:cmd:method:ReplicationPool.AddLargeWorker", - "cmd:cmd:method:ReplicationPool.AddMRFWorker", - "cmd:cmd:method:ReplicationPool.AddWorker", - "cmd:cmd:method:ReplicationPool.ResizeFailedWorkers", - "cmd:cmd:method:ReplicationPool.ResizeLrgWorkers", - "cmd:cmd:method:ReplicationPool.ResizeWorkerPriority", - "cmd:cmd:method:ReplicationPool.ResizeWorkers", - "cmd:cmd:method:ReplicationQueueStats.DecodeMsg", - "cmd:cmd:method:ReplicationQueueStats.EncodeMsg", - "cmd:cmd:method:ReplicationQueueStats.MarshalMsg", - "cmd:cmd:method:ReplicationQueueStats.Msgsize", - "cmd:cmd:method:ReplicationQueueStats.UnmarshalMsg", - "cmd:cmd:method:ReplicationState.CompositeReplicationStatus", - "cmd:cmd:method:ReplicationState.CompositeVersionPurgeStatus", - "cmd:cmd:method:ReplicationState.DecodeMsg", - "cmd:cmd:method:ReplicationState.EncodeMsg", - "cmd:cmd:method:ReplicationState.Equal", - "cmd:cmd:method:ReplicationState.MarshalMsg", - "cmd:cmd:method:ReplicationState.Msgsize", - "cmd:cmd:method:ReplicationState.UnmarshalMsg", - "cmd:cmd:method:ReplicationStats.ActiveWorkers", - "cmd:cmd:method:ReplicationStats.Delete", - "cmd:cmd:method:ReplicationStats.Get", - "cmd:cmd:method:ReplicationStats.GetAll", - "cmd:cmd:method:ReplicationStats.Update", - "cmd:cmd:method:ReplicationStats.UpdateReplicaStat", - "cmd:cmd:method:ResyncDecision.DecodeMsg", - "cmd:cmd:method:ResyncDecision.Empty", - "cmd:cmd:method:ResyncDecision.EncodeMsg", - "cmd:cmd:method:ResyncDecision.MarshalMsg", - "cmd:cmd:method:ResyncDecision.Msgsize", - "cmd:cmd:method:ResyncDecision.UnmarshalMsg", - "cmd:cmd:method:ResyncStatusType.DecodeMsg", - "cmd:cmd:method:ResyncStatusType.EncodeMsg", - "cmd:cmd:method:ResyncStatusType.MarshalMsg", - "cmd:cmd:method:ResyncStatusType.Msgsize", - "cmd:cmd:method:ResyncStatusType.String", - "cmd:cmd:method:ResyncStatusType.UnmarshalMsg", - "cmd:cmd:method:ResyncTarget.DecodeMsg", - "cmd:cmd:method:ResyncTarget.EncodeMsg", - "cmd:cmd:method:ResyncTarget.MarshalMsg", - "cmd:cmd:method:ResyncTarget.Msgsize", - "cmd:cmd:method:ResyncTarget.UnmarshalMsg", - "cmd:cmd:method:ResyncTargetDecision.DecodeMsg", - "cmd:cmd:method:ResyncTargetDecision.EncodeMsg", - "cmd:cmd:method:ResyncTargetDecision.MarshalMsg", - "cmd:cmd:method:ResyncTargetDecision.Msgsize", - "cmd:cmd:method:ResyncTargetDecision.UnmarshalMsg", - "cmd:cmd:method:ResyncTargetsInfo.DecodeMsg", - "cmd:cmd:method:ResyncTargetsInfo.EncodeMsg", - "cmd:cmd:method:ResyncTargetsInfo.MarshalMsg", - "cmd:cmd:method:ResyncTargetsInfo.Msgsize", - "cmd:cmd:method:ResyncTargetsInfo.UnmarshalMsg", - "cmd:cmd:method:S3PeerSys.DeleteBucket", - "cmd:cmd:method:S3PeerSys.GetBucketInfo", - "cmd:cmd:method:S3PeerSys.HealBucket", - "cmd:cmd:method:S3PeerSys.ListBuckets", - "cmd:cmd:method:S3PeerSys.MakeBucket", - "cmd:cmd:method:SMA.DecodeMsg", - "cmd:cmd:method:SMA.EncodeMsg", - "cmd:cmd:method:SMA.MarshalMsg", - "cmd:cmd:method:SMA.Msgsize", - "cmd:cmd:method:SMA.UnmarshalMsg", - "cmd:cmd:method:SRBucketDeleteOp.Empty", - "cmd:cmd:method:SRError.Error", - "cmd:cmd:method:SRError.Unwrap", - "cmd:cmd:method:SRMetric.DecodeMsg", - "cmd:cmd:method:SRMetric.EncodeMsg", - "cmd:cmd:method:SRMetric.MarshalMsg", - "cmd:cmd:method:SRMetric.Msgsize", - "cmd:cmd:method:SRMetric.UnmarshalMsg", - "cmd:cmd:method:SRMetricsSummary.DecodeMsg", - "cmd:cmd:method:SRMetricsSummary.EncodeMsg", - "cmd:cmd:method:SRMetricsSummary.MarshalMsg", - "cmd:cmd:method:SRMetricsSummary.Msgsize", - "cmd:cmd:method:SRMetricsSummary.UnmarshalMsg", - "cmd:cmd:method:SRStats.DecodeMsg", - "cmd:cmd:method:SRStats.EncodeMsg", - "cmd:cmd:method:SRStats.MarshalMsg", - "cmd:cmd:method:SRStats.Msgsize", - "cmd:cmd:method:SRStats.UnmarshalMsg", - "cmd:cmd:method:SRStatus.DecodeMsg", - "cmd:cmd:method:SRStatus.EncodeMsg", - "cmd:cmd:method:SRStatus.MarshalMsg", - "cmd:cmd:method:SRStatus.Msgsize", - "cmd:cmd:method:SRStatus.UnmarshalMsg", - "cmd:cmd:method:STSErrorCode.String", - "cmd:cmd:method:SelectParameters.IsEmpty", - "cmd:cmd:method:SelectParameters.UnmarshalXML", - "cmd:cmd:method:ServerSystemConfig.DecodeMsg", - "cmd:cmd:method:ServerSystemConfig.Diff", - "cmd:cmd:method:ServerSystemConfig.EncodeMsg", - "cmd:cmd:method:ServerSystemConfig.MarshalMsg", - "cmd:cmd:method:ServerSystemConfig.Msgsize", - "cmd:cmd:method:ServerSystemConfig.UnmarshalMsg", - "cmd:cmd:method:SetupType.String", - "cmd:cmd:method:SignatureDoesNotMatch.Error", - "cmd:cmd:method:SiteReplicationSys.AddPeerClusters", - "cmd:cmd:method:SiteReplicationSys.BucketMetaHook", - "cmd:cmd:method:SiteReplicationSys.DeleteBucketHook", - "cmd:cmd:method:SiteReplicationSys.EditPeerCluster", - "cmd:cmd:method:SiteReplicationSys.GetClusterInfo", - "cmd:cmd:method:SiteReplicationSys.GetIDPSettings", - "cmd:cmd:method:SiteReplicationSys.IAMChangeHook", - "cmd:cmd:method:SiteReplicationSys.Init", - "cmd:cmd:method:SiteReplicationSys.InternalRemoveReq", - "cmd:cmd:method:SiteReplicationSys.MakeBucketHook", - "cmd:cmd:method:SiteReplicationSys.Netperf", - "cmd:cmd:method:SiteReplicationSys.PeerAddPolicyHandler", - "cmd:cmd:method:SiteReplicationSys.PeerBucketConfigureReplHandler", - "cmd:cmd:method:SiteReplicationSys.PeerBucketDeleteHandler", - "cmd:cmd:method:SiteReplicationSys.PeerBucketLCConfigHandler", - "cmd:cmd:method:SiteReplicationSys.PeerBucketMakeWithVersioningHandler", - "cmd:cmd:method:SiteReplicationSys.PeerBucketMetadataUpdateHandler", - "cmd:cmd:method:SiteReplicationSys.PeerBucketObjectLockConfigHandler", - "cmd:cmd:method:SiteReplicationSys.PeerBucketPolicyHandler", - "cmd:cmd:method:SiteReplicationSys.PeerBucketQuotaConfigHandler", - "cmd:cmd:method:SiteReplicationSys.PeerBucketSSEConfigHandler", - "cmd:cmd:method:SiteReplicationSys.PeerBucketTaggingHandler", - "cmd:cmd:method:SiteReplicationSys.PeerBucketVersioningHandler", - "cmd:cmd:method:SiteReplicationSys.PeerEditReq", - "cmd:cmd:method:SiteReplicationSys.PeerGroupInfoChangeHandler", - "cmd:cmd:method:SiteReplicationSys.PeerIAMUserChangeHandler", - "cmd:cmd:method:SiteReplicationSys.PeerJoinReq", - "cmd:cmd:method:SiteReplicationSys.PeerPolicyMappingHandler", - "cmd:cmd:method:SiteReplicationSys.PeerSTSAccHandler", - "cmd:cmd:method:SiteReplicationSys.PeerStateEditReq", - "cmd:cmd:method:SiteReplicationSys.PeerSvcAccChangeHandler", - "cmd:cmd:method:SiteReplicationSys.RemovePeerCluster", - "cmd:cmd:method:SiteReplicationSys.RemoveRemoteTargetsForEndpoint", - "cmd:cmd:method:SiteReplicationSys.SiteReplicationMetaInfo", - "cmd:cmd:method:SiteReplicationSys.SiteReplicationStatus", - "cmd:cmd:method:SiteResyncStatus.DecodeMsg", - "cmd:cmd:method:SiteResyncStatus.EncodeMsg", - "cmd:cmd:method:SiteResyncStatus.MarshalMsg", - "cmd:cmd:method:SiteResyncStatus.Msgsize", - "cmd:cmd:method:SiteResyncStatus.UnmarshalMsg", - "cmd:cmd:method:SlowDown.Error", - "cmd:cmd:method:StatInfo.DecodeMsg", - "cmd:cmd:method:StatInfo.EncodeMsg", - "cmd:cmd:method:StatInfo.MarshalMsg", - "cmd:cmd:method:StatInfo.Msgsize", - "cmd:cmd:method:StatInfo.UnmarshalMsg", - "cmd:cmd:method:StorageErr.Error", - "cmd:cmd:method:StorageFull.Error", - "cmd:cmd:method:TargetReplicationResyncStatus.DecodeMsg", - "cmd:cmd:method:TargetReplicationResyncStatus.EncodeMsg", - "cmd:cmd:method:TargetReplicationResyncStatus.MarshalMsg", - "cmd:cmd:method:TargetReplicationResyncStatus.Msgsize", - "cmd:cmd:method:TargetReplicationResyncStatus.UnmarshalMsg", - "cmd:cmd:method:TierConfigMgr.Add", - "cmd:cmd:method:TierConfigMgr.Bytes", - "cmd:cmd:method:TierConfigMgr.DecodeMsg", - "cmd:cmd:method:TierConfigMgr.Edit", - "cmd:cmd:method:TierConfigMgr.Empty", - "cmd:cmd:method:TierConfigMgr.EncodeMsg", - "cmd:cmd:method:TierConfigMgr.Init", - "cmd:cmd:method:TierConfigMgr.IsTierValid", - "cmd:cmd:method:TierConfigMgr.ListTiers", - "cmd:cmd:method:TierConfigMgr.MarshalMsg", - "cmd:cmd:method:TierConfigMgr.Msgsize", - "cmd:cmd:method:TierConfigMgr.Reload", - "cmd:cmd:method:TierConfigMgr.Remove", - "cmd:cmd:method:TierConfigMgr.Save", - "cmd:cmd:method:TierConfigMgr.TierType", - "cmd:cmd:method:TierConfigMgr.UnmarshalMsg", - "cmd:cmd:method:TierConfigMgr.Verify", - "cmd:cmd:method:TransitionStorageClassNotFound.Error", - "cmd:cmd:method:TransitionedObject.MarshalMsg", - "cmd:cmd:method:TransitionedObject.Msgsize", - "cmd:cmd:method:TransitionedObject.UnmarshalMsg", - "cmd:cmd:method:UnsupportedMetadata.Error", - "cmd:cmd:method:UpdateMetadataOpts.DecodeMsg", - "cmd:cmd:method:UpdateMetadataOpts.EncodeMsg", - "cmd:cmd:method:UpdateMetadataOpts.MarshalMsg", - "cmd:cmd:method:UpdateMetadataOpts.Msgsize", - "cmd:cmd:method:UpdateMetadataOpts.UnmarshalMsg", - "cmd:cmd:method:VersionNotFound.Error", - "cmd:cmd:method:VersionType.DecodeMsg", - "cmd:cmd:method:VersionType.EncodeMsg", - "cmd:cmd:method:VersionType.MarshalMsg", - "cmd:cmd:method:VersionType.Msgsize", - "cmd:cmd:method:VersionType.String", - "cmd:cmd:method:VersionType.UnmarshalMsg", - "cmd:cmd:method:VolInfo.DecodeMsg", - "cmd:cmd:method:VolInfo.EncodeMsg", - "cmd:cmd:method:VolInfo.MarshalMsg", - "cmd:cmd:method:VolInfo.Msgsize", - "cmd:cmd:method:VolInfo.UnmarshalMsg", - "cmd:cmd:method:VolsInfo.DecodeMsg", - "cmd:cmd:method:VolsInfo.EncodeMsg", - "cmd:cmd:method:VolsInfo.MarshalMsg", - "cmd:cmd:method:VolsInfo.Msgsize", - "cmd:cmd:method:VolsInfo.UnmarshalMsg", - "cmd:cmd:method:WalkDirOptions.DecodeMsg", - "cmd:cmd:method:WalkDirOptions.EncodeMsg", - "cmd:cmd:method:WalkDirOptions.MarshalMsg", - "cmd:cmd:method:WalkDirOptions.Msgsize", - "cmd:cmd:method:WalkDirOptions.UnmarshalMsg", - "cmd:cmd:method:WalkOptions.MarshalMsg", - "cmd:cmd:method:WalkOptions.Msgsize", - "cmd:cmd:method:WalkOptions.UnmarshalMsg", - "cmd:cmd:method:WalkVersionsSortOrder.MarshalMsg", - "cmd:cmd:method:WalkVersionsSortOrder.Msgsize", - "cmd:cmd:method:WalkVersionsSortOrder.UnmarshalMsg", - "cmd:cmd:method:WriteAllHandlerParams.DecodeMsg", - "cmd:cmd:method:WriteAllHandlerParams.EncodeMsg", - "cmd:cmd:method:WriteAllHandlerParams.MarshalMsg", - "cmd:cmd:method:WriteAllHandlerParams.Msgsize", - "cmd:cmd:method:WriteAllHandlerParams.UnmarshalMsg", - "cmd:cmd:method:XferStats.Clone", - "cmd:cmd:method:XferStats.DecodeMsg", - "cmd:cmd:method:XferStats.EncodeMsg", - "cmd:cmd:method:XferStats.MarshalMsg", - "cmd:cmd:method:XferStats.Msgsize", - "cmd:cmd:method:XferStats.String", - "cmd:cmd:method:XferStats.UnmarshalMsg", - "cmd:cmd:method:adminAPIHandlers.AccountInfoHandler", - "cmd:cmd:method:adminAPIHandlers.AddCannedPolicy", - "cmd:cmd:method:adminAPIHandlers.AddIdentityProviderCfg", - "cmd:cmd:method:adminAPIHandlers.AddServiceAccount", - "cmd:cmd:method:adminAPIHandlers.AddServiceAccountLDAP", - "cmd:cmd:method:adminAPIHandlers.AddTierHandler", - "cmd:cmd:method:adminAPIHandlers.AddUser", - "cmd:cmd:method:adminAPIHandlers.AttachDetachPolicyBuiltin", - "cmd:cmd:method:adminAPIHandlers.AttachDetachPolicyLDAP", - "cmd:cmd:method:adminAPIHandlers.BackgroundHealStatusHandler", - "cmd:cmd:method:adminAPIHandlers.BatchJobStatus", - "cmd:cmd:method:adminAPIHandlers.CancelBatchJob", - "cmd:cmd:method:adminAPIHandlers.CancelDecommission", - "cmd:cmd:method:adminAPIHandlers.ClearConfigHistoryKVHandler", - "cmd:cmd:method:adminAPIHandlers.ClientDevNull", - "cmd:cmd:method:adminAPIHandlers.ClientDevNullExtraTime", - "cmd:cmd:method:adminAPIHandlers.ConsoleLogHandler", - "cmd:cmd:method:adminAPIHandlers.DataUsageInfoHandler", - "cmd:cmd:method:adminAPIHandlers.DelConfigKVHandler", - "cmd:cmd:method:adminAPIHandlers.DeleteIdentityProviderCfg", - "cmd:cmd:method:adminAPIHandlers.DeleteServiceAccount", - "cmd:cmd:method:adminAPIHandlers.DescribeBatchJob", - "cmd:cmd:method:adminAPIHandlers.DownloadProfilingHandler", - "cmd:cmd:method:adminAPIHandlers.DriveSpeedtestHandler", - "cmd:cmd:method:adminAPIHandlers.EditTierHandler", - "cmd:cmd:method:adminAPIHandlers.ExportBucketMetadataHandler", - "cmd:cmd:method:adminAPIHandlers.ExportIAM", - "cmd:cmd:method:adminAPIHandlers.ForceUnlockHandler", - "cmd:cmd:method:adminAPIHandlers.GetBucketQuotaConfigHandler", - "cmd:cmd:method:adminAPIHandlers.GetConfigHandler", - "cmd:cmd:method:adminAPIHandlers.GetConfigKVHandler", - "cmd:cmd:method:adminAPIHandlers.GetGroup", - "cmd:cmd:method:adminAPIHandlers.GetIdentityProviderCfg", - "cmd:cmd:method:adminAPIHandlers.GetUserInfo", - "cmd:cmd:method:adminAPIHandlers.HealHandler", - "cmd:cmd:method:adminAPIHandlers.HealthInfoHandler", - "cmd:cmd:method:adminAPIHandlers.HelpConfigKVHandler", - "cmd:cmd:method:adminAPIHandlers.ImportBucketMetadataHandler", - "cmd:cmd:method:adminAPIHandlers.ImportIAM", - "cmd:cmd:method:adminAPIHandlers.ImportIAMV2", - "cmd:cmd:method:adminAPIHandlers.InfoAccessKey", - "cmd:cmd:method:adminAPIHandlers.InfoCannedPolicy", - "cmd:cmd:method:adminAPIHandlers.InfoServiceAccount", - "cmd:cmd:method:adminAPIHandlers.InspectDataHandler", - "cmd:cmd:method:adminAPIHandlers.KMSCreateKeyHandler", - "cmd:cmd:method:adminAPIHandlers.KMSKeyStatusHandler", - "cmd:cmd:method:adminAPIHandlers.KMSStatusHandler", - "cmd:cmd:method:adminAPIHandlers.ListAccessKeysBulk", - "cmd:cmd:method:adminAPIHandlers.ListAccessKeysLDAP", - "cmd:cmd:method:adminAPIHandlers.ListAccessKeysLDAPBulk", - "cmd:cmd:method:adminAPIHandlers.ListAccessKeysOpenIDBulk", - "cmd:cmd:method:adminAPIHandlers.ListBatchJobs", - "cmd:cmd:method:adminAPIHandlers.ListBucketPolicies", - "cmd:cmd:method:adminAPIHandlers.ListBucketUsers", - "cmd:cmd:method:adminAPIHandlers.ListCannedPolicies", - "cmd:cmd:method:adminAPIHandlers.ListConfigHistoryKVHandler", - "cmd:cmd:method:adminAPIHandlers.ListGroups", - "cmd:cmd:method:adminAPIHandlers.ListIdentityProviderCfg", - "cmd:cmd:method:adminAPIHandlers.ListLDAPPolicyMappingEntities", - "cmd:cmd:method:adminAPIHandlers.ListPolicyMappingEntities", - "cmd:cmd:method:adminAPIHandlers.ListPools", - "cmd:cmd:method:adminAPIHandlers.ListRemoteTargetsHandler", - "cmd:cmd:method:adminAPIHandlers.ListServiceAccounts", - "cmd:cmd:method:adminAPIHandlers.ListTierHandler", - "cmd:cmd:method:adminAPIHandlers.ListUsers", - "cmd:cmd:method:adminAPIHandlers.MetricsHandler", - "cmd:cmd:method:adminAPIHandlers.NetperfHandler", - "cmd:cmd:method:adminAPIHandlers.ObjectSpeedTestHandler", - "cmd:cmd:method:adminAPIHandlers.ProfileHandler", - "cmd:cmd:method:adminAPIHandlers.PutBucketQuotaConfigHandler", - "cmd:cmd:method:adminAPIHandlers.RebalanceStart", - "cmd:cmd:method:adminAPIHandlers.RebalanceStatus", - "cmd:cmd:method:adminAPIHandlers.RebalanceStop", - "cmd:cmd:method:adminAPIHandlers.RemoveCannedPolicy", - "cmd:cmd:method:adminAPIHandlers.RemoveRemoteTargetHandler", - "cmd:cmd:method:adminAPIHandlers.RemoveTierHandler", - "cmd:cmd:method:adminAPIHandlers.RemoveUser", - "cmd:cmd:method:adminAPIHandlers.ReplicationDiffHandler", - "cmd:cmd:method:adminAPIHandlers.ReplicationMRFHandler", - "cmd:cmd:method:adminAPIHandlers.RestoreConfigHistoryKVHandler", - "cmd:cmd:method:adminAPIHandlers.RevokeTokens", - "cmd:cmd:method:adminAPIHandlers.SRPeerBucketOps", - "cmd:cmd:method:adminAPIHandlers.SRPeerEdit", - "cmd:cmd:method:adminAPIHandlers.SRPeerGetIDPSettings", - "cmd:cmd:method:adminAPIHandlers.SRPeerJoin", - "cmd:cmd:method:adminAPIHandlers.SRPeerRemove", - "cmd:cmd:method:adminAPIHandlers.SRPeerReplicateBucketItem", - "cmd:cmd:method:adminAPIHandlers.SRPeerReplicateIAMItem", - "cmd:cmd:method:adminAPIHandlers.SRStateEdit", - "cmd:cmd:method:adminAPIHandlers.ServerInfoHandler", - "cmd:cmd:method:adminAPIHandlers.ServerUpdateHandler", - "cmd:cmd:method:adminAPIHandlers.ServerUpdateV2Handler", - "cmd:cmd:method:adminAPIHandlers.ServiceHandler", - "cmd:cmd:method:adminAPIHandlers.ServiceV2Handler", - "cmd:cmd:method:adminAPIHandlers.SetConfigHandler", - "cmd:cmd:method:adminAPIHandlers.SetConfigKVHandler", - "cmd:cmd:method:adminAPIHandlers.SetGroupStatus", - "cmd:cmd:method:adminAPIHandlers.SetPolicyForUserOrGroup", - "cmd:cmd:method:adminAPIHandlers.SetRemoteTargetHandler", - "cmd:cmd:method:adminAPIHandlers.SetUserStatus", - "cmd:cmd:method:adminAPIHandlers.SitePerfHandler", - "cmd:cmd:method:adminAPIHandlers.SiteReplicationAdd", - "cmd:cmd:method:adminAPIHandlers.SiteReplicationDevNull", - "cmd:cmd:method:adminAPIHandlers.SiteReplicationEdit", - "cmd:cmd:method:adminAPIHandlers.SiteReplicationInfo", - "cmd:cmd:method:adminAPIHandlers.SiteReplicationMetaInfo", - "cmd:cmd:method:adminAPIHandlers.SiteReplicationNetPerf", - "cmd:cmd:method:adminAPIHandlers.SiteReplicationRemove", - "cmd:cmd:method:adminAPIHandlers.SiteReplicationResyncOp", - "cmd:cmd:method:adminAPIHandlers.SiteReplicationStatus", - "cmd:cmd:method:adminAPIHandlers.StartBatchJob", - "cmd:cmd:method:adminAPIHandlers.StartDecommission", - "cmd:cmd:method:adminAPIHandlers.StartProfilingHandler", - "cmd:cmd:method:adminAPIHandlers.StatusPool", - "cmd:cmd:method:adminAPIHandlers.StorageInfoHandler", - "cmd:cmd:method:adminAPIHandlers.TemporaryAccountInfo", - "cmd:cmd:method:adminAPIHandlers.TierStatsHandler", - "cmd:cmd:method:adminAPIHandlers.TopLocksHandler", - "cmd:cmd:method:adminAPIHandlers.TraceHandler", - "cmd:cmd:method:adminAPIHandlers.UpdateGroupMembers", - "cmd:cmd:method:adminAPIHandlers.UpdateIdentityProviderCfg", - "cmd:cmd:method:adminAPIHandlers.UpdateServiceAccount", - "cmd:cmd:method:adminAPIHandlers.VerifyTierHandler", - "cmd:cmd:method:allHealState.LaunchNewHealSequence", - "cmd:cmd:method:allHealState.PopHealStatusJSON", - "cmd:cmd:method:allTierStats.DecodeMsg", - "cmd:cmd:method:allTierStats.EncodeMsg", - "cmd:cmd:method:allTierStats.MarshalMsg", - "cmd:cmd:method:allTierStats.Msgsize", - "cmd:cmd:method:allTierStats.UnmarshalMsg", - "cmd:cmd:method:auditObjectOp.String", - "cmd:cmd:method:auditTierOp.String", - "cmd:cmd:method:authType.String", - "cmd:cmd:method:azureConf.NewClient", - "cmd:cmd:method:azureConf.Validate", - "cmd:cmd:method:badConfigErr.Error", - "cmd:cmd:method:badConfigErr.Unwrap", - "cmd:cmd:method:batchExpireJobError.Error", - "cmd:cmd:method:batchJobInfo.DecodeMsg", - "cmd:cmd:method:batchJobInfo.EncodeMsg", - "cmd:cmd:method:batchJobInfo.MarshalMsg", - "cmd:cmd:method:batchJobInfo.Msgsize", - "cmd:cmd:method:batchJobInfo.UnmarshalMsg", - "cmd:cmd:method:batchJobMetric.String", - "cmd:cmd:method:batchKeyRotationJobError.Error", - "cmd:cmd:method:batchReplicationJobError.Error", - "cmd:cmd:method:bgCtx.Deadline", - "cmd:cmd:method:bgCtx.Done", - "cmd:cmd:method:bgCtx.Err", - "cmd:cmd:method:bgCtx.Value", - "cmd:cmd:method:bootstrapRESTClient.String", - "cmd:cmd:method:bootstrapRESTClient.Verify", - "cmd:cmd:method:bootstrapRESTServer.VerifyHandler", - "cmd:cmd:method:bootstrapTracer.Events", - "cmd:cmd:method:bootstrapTracer.Publish", - "cmd:cmd:method:bootstrapTracer.Record", - "cmd:cmd:method:caseInsensitiveMap.Lookup", - "cmd:cmd:method:checksumInfoJSON.DecodeMsg", - "cmd:cmd:method:checksumInfoJSON.EncodeMsg", - "cmd:cmd:method:checksumInfoJSON.MarshalMsg", - "cmd:cmd:method:checksumInfoJSON.Msgsize", - "cmd:cmd:method:checksumInfoJSON.UnmarshalMsg", - "cmd:cmd:method:closeNotifier.Close", - "cmd:cmd:method:closeNotifier.Read", - "cmd:cmd:method:concErr.Error", - "cmd:cmd:method:concErr.Unwrap", - "cmd:cmd:method:counterMap.GetValueWithQuorum", - "cmd:cmd:method:currentScannerCycle.MarshalMsg", - "cmd:cmd:method:currentScannerCycle.Msgsize", - "cmd:cmd:method:currentScannerCycle.UnmarshalMsg", - "cmd:cmd:method:dataUsageCache.DecodeMsg", - "cmd:cmd:method:dataUsageCache.EncodeMsg", - "cmd:cmd:method:dataUsageCache.MarshalMsg", - "cmd:cmd:method:dataUsageCache.Msgsize", - "cmd:cmd:method:dataUsageCache.StringAll", - "cmd:cmd:method:dataUsageCache.UnmarshalMsg", - "cmd:cmd:method:dataUsageCacheInfo.DecodeMsg", - "cmd:cmd:method:dataUsageCacheInfo.EncodeMsg", - "cmd:cmd:method:dataUsageCacheInfo.MarshalMsg", - "cmd:cmd:method:dataUsageCacheInfo.Msgsize", - "cmd:cmd:method:dataUsageCacheInfo.UnmarshalMsg", - "cmd:cmd:method:dataUsageCacheV2.DecodeMsg", - "cmd:cmd:method:dataUsageCacheV2.Msgsize", - "cmd:cmd:method:dataUsageCacheV2.UnmarshalMsg", - "cmd:cmd:method:dataUsageCacheV3.DecodeMsg", - "cmd:cmd:method:dataUsageCacheV3.Msgsize", - "cmd:cmd:method:dataUsageCacheV3.UnmarshalMsg", - "cmd:cmd:method:dataUsageCacheV4.DecodeMsg", - "cmd:cmd:method:dataUsageCacheV4.Msgsize", - "cmd:cmd:method:dataUsageCacheV4.UnmarshalMsg", - "cmd:cmd:method:dataUsageCacheV5.DecodeMsg", - "cmd:cmd:method:dataUsageCacheV5.Msgsize", - "cmd:cmd:method:dataUsageCacheV5.UnmarshalMsg", - "cmd:cmd:method:dataUsageCacheV6.DecodeMsg", - "cmd:cmd:method:dataUsageCacheV6.Msgsize", - "cmd:cmd:method:dataUsageCacheV6.UnmarshalMsg", - "cmd:cmd:method:dataUsageCacheV7.DecodeMsg", - "cmd:cmd:method:dataUsageCacheV7.Msgsize", - "cmd:cmd:method:dataUsageCacheV7.UnmarshalMsg", - "cmd:cmd:method:dataUsageEntry.DecodeMsg", - "cmd:cmd:method:dataUsageEntry.EncodeMsg", - "cmd:cmd:method:dataUsageEntry.MarshalMsg", - "cmd:cmd:method:dataUsageEntry.Msgsize", - "cmd:cmd:method:dataUsageEntry.UnmarshalMsg", - "cmd:cmd:method:dataUsageEntryV2.DecodeMsg", - "cmd:cmd:method:dataUsageEntryV2.Msgsize", - "cmd:cmd:method:dataUsageEntryV2.UnmarshalMsg", - "cmd:cmd:method:dataUsageEntryV3.DecodeMsg", - "cmd:cmd:method:dataUsageEntryV3.Msgsize", - "cmd:cmd:method:dataUsageEntryV3.UnmarshalMsg", - "cmd:cmd:method:dataUsageEntryV4.DecodeMsg", - "cmd:cmd:method:dataUsageEntryV4.Msgsize", - "cmd:cmd:method:dataUsageEntryV4.UnmarshalMsg", - "cmd:cmd:method:dataUsageEntryV5.DecodeMsg", - "cmd:cmd:method:dataUsageEntryV5.Msgsize", - "cmd:cmd:method:dataUsageEntryV5.UnmarshalMsg", - "cmd:cmd:method:dataUsageEntryV6.DecodeMsg", - "cmd:cmd:method:dataUsageEntryV6.Msgsize", - "cmd:cmd:method:dataUsageEntryV6.UnmarshalMsg", - "cmd:cmd:method:dataUsageEntryV7.DecodeMsg", - "cmd:cmd:method:dataUsageEntryV7.Msgsize", - "cmd:cmd:method:dataUsageEntryV7.UnmarshalMsg", - "cmd:cmd:method:dataUsageHash.DecodeMsg", - "cmd:cmd:method:dataUsageHash.EncodeMsg", - "cmd:cmd:method:dataUsageHash.Key", - "cmd:cmd:method:dataUsageHash.MarshalMsg", - "cmd:cmd:method:dataUsageHash.Msgsize", - "cmd:cmd:method:dataUsageHash.String", - "cmd:cmd:method:dataUsageHash.UnmarshalMsg", - "cmd:cmd:method:dataUsageHashMap.DecodeMsg", - "cmd:cmd:method:dataUsageHashMap.EncodeMsg", - "cmd:cmd:method:dataUsageHashMap.MarshalMsg", - "cmd:cmd:method:dataUsageHashMap.Msgsize", - "cmd:cmd:method:dataUsageHashMap.UnmarshalMsg", - "cmd:cmd:method:decomBucketInfo.String", - "cmd:cmd:method:decomError.DecodeMsg", - "cmd:cmd:method:decomError.EncodeMsg", - "cmd:cmd:method:decomError.Error", - "cmd:cmd:method:decomError.MarshalMsg", - "cmd:cmd:method:decomError.Msgsize", - "cmd:cmd:method:decomError.UnmarshalMsg", - "cmd:cmd:method:decomMetric.String", - "cmd:cmd:method:disconnectReader.Close", - "cmd:cmd:method:disconnectReader.Read", - "cmd:cmd:method:diskHealthWrapper.Read", - "cmd:cmd:method:diskHealthWrapper.Write", - "cmd:cmd:method:distLockInstance.GetLock", - "cmd:cmd:method:distLockInstance.GetRLock", - "cmd:cmd:method:distLockInstance.RUnlock", - "cmd:cmd:method:distLockInstance.Unlock", - "cmd:cmd:method:dummyFileInfo.IsDir", - "cmd:cmd:method:dummyFileInfo.ModTime", - "cmd:cmd:method:dummyFileInfo.Mode", - "cmd:cmd:method:dummyFileInfo.Name", - "cmd:cmd:method:dummyFileInfo.Size", - "cmd:cmd:method:dummyFileInfo.Sys", - "cmd:cmd:method:dynamicSleeper.Sleep", - "cmd:cmd:method:dynamicSleeper.Timer", - "cmd:cmd:method:dynamicSleeper.Update", - "cmd:cmd:method:dynamicTimeout.LogFailure", - "cmd:cmd:method:dynamicTimeout.LogSuccess", - "cmd:cmd:method:dynamicTimeout.RetryInterval", - "cmd:cmd:method:dynamicTimeout.Timeout", - "cmd:cmd:method:endpointSet.Get", - "cmd:cmd:method:envKV.String", - "cmd:cmd:method:erasureObjects.AbortMultipartUpload", - "cmd:cmd:method:erasureObjects.CompleteMultipartUpload", - "cmd:cmd:method:erasureObjects.CopyObject", - "cmd:cmd:method:erasureObjects.DecomTieredObject", - "cmd:cmd:method:erasureObjects.DeleteObject", - "cmd:cmd:method:erasureObjects.DeleteObjectTags", - "cmd:cmd:method:erasureObjects.DeleteObjects", - "cmd:cmd:method:erasureObjects.GetMultipartInfo", - "cmd:cmd:method:erasureObjects.GetObjectInfo", - "cmd:cmd:method:erasureObjects.GetObjectNInfo", - "cmd:cmd:method:erasureObjects.GetObjectTags", - "cmd:cmd:method:erasureObjects.HealObject", - "cmd:cmd:method:erasureObjects.ListMultipartUploads", - "cmd:cmd:method:erasureObjects.ListObjectParts", - "cmd:cmd:method:erasureObjects.LocalStorageInfo", - "cmd:cmd:method:erasureObjects.NewMultipartUpload", - "cmd:cmd:method:erasureObjects.NewNSLock", - "cmd:cmd:method:erasureObjects.PutObject", - "cmd:cmd:method:erasureObjects.PutObjectMetadata", - "cmd:cmd:method:erasureObjects.PutObjectPart", - "cmd:cmd:method:erasureObjects.PutObjectTags", - "cmd:cmd:method:erasureObjects.RestoreTransitionedObject", - "cmd:cmd:method:erasureObjects.Shutdown", - "cmd:cmd:method:erasureObjects.StorageInfo", - "cmd:cmd:method:erasureObjects.TransitionObject", - "cmd:cmd:method:erasureServerPools.AbortMultipartUpload", - "cmd:cmd:method:erasureServerPools.BackendInfo", - "cmd:cmd:method:erasureServerPools.CheckAbandonedParts", - "cmd:cmd:method:erasureServerPools.ClearUploadID", - "cmd:cmd:method:erasureServerPools.CompleteDecommission", - "cmd:cmd:method:erasureServerPools.CompleteMultipartUpload", - "cmd:cmd:method:erasureServerPools.CopyObject", - "cmd:cmd:method:erasureServerPools.CopyObjectPart", - "cmd:cmd:method:erasureServerPools.DecomTieredObject", - "cmd:cmd:method:erasureServerPools.Decommission", - "cmd:cmd:method:erasureServerPools.DecommissionCancel", - "cmd:cmd:method:erasureServerPools.DecommissionFailed", - "cmd:cmd:method:erasureServerPools.DeleteBucket", - "cmd:cmd:method:erasureServerPools.DeleteObject", - "cmd:cmd:method:erasureServerPools.DeleteObjectTags", - "cmd:cmd:method:erasureServerPools.DeleteObjects", - "cmd:cmd:method:erasureServerPools.GetBucketInfo", - "cmd:cmd:method:erasureServerPools.GetDisks", - "cmd:cmd:method:erasureServerPools.GetDisksID", - "cmd:cmd:method:erasureServerPools.GetMultipartInfo", - "cmd:cmd:method:erasureServerPools.GetObjectInfo", - "cmd:cmd:method:erasureServerPools.GetObjectNInfo", - "cmd:cmd:method:erasureServerPools.GetObjectTags", - "cmd:cmd:method:erasureServerPools.GetRawData", - "cmd:cmd:method:erasureServerPools.HealBucket", - "cmd:cmd:method:erasureServerPools.HealFormat", - "cmd:cmd:method:erasureServerPools.HealObject", - "cmd:cmd:method:erasureServerPools.HealObjects", - "cmd:cmd:method:erasureServerPools.Health", - "cmd:cmd:method:erasureServerPools.Init", - "cmd:cmd:method:erasureServerPools.IsDecommissionRunning", - "cmd:cmd:method:erasureServerPools.IsPoolRebalancing", - "cmd:cmd:method:erasureServerPools.IsRebalanceStarted", - "cmd:cmd:method:erasureServerPools.IsSuspended", - "cmd:cmd:method:erasureServerPools.Legacy", - "cmd:cmd:method:erasureServerPools.ListBuckets", - "cmd:cmd:method:erasureServerPools.ListMultipartUploads", - "cmd:cmd:method:erasureServerPools.ListObjectParts", - "cmd:cmd:method:erasureServerPools.ListObjectVersions", - "cmd:cmd:method:erasureServerPools.ListObjects", - "cmd:cmd:method:erasureServerPools.ListObjectsV2", - "cmd:cmd:method:erasureServerPools.LocalStorageInfo", - "cmd:cmd:method:erasureServerPools.MakeBucket", - "cmd:cmd:method:erasureServerPools.NSScanner", - "cmd:cmd:method:erasureServerPools.NewMultipartUpload", - "cmd:cmd:method:erasureServerPools.NewNSLock", - "cmd:cmd:method:erasureServerPools.PutObject", - "cmd:cmd:method:erasureServerPools.PutObjectMetadata", - "cmd:cmd:method:erasureServerPools.PutObjectPart", - "cmd:cmd:method:erasureServerPools.PutObjectTags", - "cmd:cmd:method:erasureServerPools.ReloadPoolMeta", - "cmd:cmd:method:erasureServerPools.RestoreTransitionedObject", - "cmd:cmd:method:erasureServerPools.SetDriveCounts", - "cmd:cmd:method:erasureServerPools.Shutdown", - "cmd:cmd:method:erasureServerPools.SinglePool", - "cmd:cmd:method:erasureServerPools.StartDecommission", - "cmd:cmd:method:erasureServerPools.StartRebalance", - "cmd:cmd:method:erasureServerPools.Status", - "cmd:cmd:method:erasureServerPools.StopRebalance", - "cmd:cmd:method:erasureServerPools.StorageInfo", - "cmd:cmd:method:erasureServerPools.TransitionObject", - "cmd:cmd:method:erasureServerPools.Walk", - "cmd:cmd:method:erasureSets.AbortMultipartUpload", - "cmd:cmd:method:erasureSets.CheckAbandonedParts", - "cmd:cmd:method:erasureSets.CompleteMultipartUpload", - "cmd:cmd:method:erasureSets.CopyObject", - "cmd:cmd:method:erasureSets.DecomTieredObject", - "cmd:cmd:method:erasureSets.DeleteObject", - "cmd:cmd:method:erasureSets.DeleteObjectTags", - "cmd:cmd:method:erasureSets.DeleteObjects", - "cmd:cmd:method:erasureSets.GetDisks", - "cmd:cmd:method:erasureSets.GetEndpointStrings", - "cmd:cmd:method:erasureSets.GetEndpoints", - "cmd:cmd:method:erasureSets.GetLockers", - "cmd:cmd:method:erasureSets.GetMultipartInfo", - "cmd:cmd:method:erasureSets.GetObjectInfo", - "cmd:cmd:method:erasureSets.GetObjectNInfo", - "cmd:cmd:method:erasureSets.GetObjectTags", - "cmd:cmd:method:erasureSets.HealFormat", - "cmd:cmd:method:erasureSets.HealObject", - "cmd:cmd:method:erasureSets.Legacy", - "cmd:cmd:method:erasureSets.ListMultipartUploads", - "cmd:cmd:method:erasureSets.ListObjectParts", - "cmd:cmd:method:erasureSets.LocalStorageInfo", - "cmd:cmd:method:erasureSets.NewMultipartUpload", - "cmd:cmd:method:erasureSets.NewNSLock", - "cmd:cmd:method:erasureSets.ParityCount", - "cmd:cmd:method:erasureSets.PutObject", - "cmd:cmd:method:erasureSets.PutObjectMetadata", - "cmd:cmd:method:erasureSets.PutObjectPart", - "cmd:cmd:method:erasureSets.PutObjectTags", - "cmd:cmd:method:erasureSets.RestoreTransitionedObject", - "cmd:cmd:method:erasureSets.SetDriveCount", - "cmd:cmd:method:erasureSets.Shutdown", - "cmd:cmd:method:erasureSets.StorageInfo", - "cmd:cmd:method:erasureSets.TransitionObject", - "cmd:cmd:method:errorCodeMap.ToAPIErr", - "cmd:cmd:method:errorCodeMap.ToAPIErrWithErr", - "cmd:cmd:method:eventArgs.ToEvent", - "cmd:cmd:method:expiryState.PendingTasks", - "cmd:cmd:method:expiryState.ResizeWorkers", - "cmd:cmd:method:expiryState.Worker", - "cmd:cmd:method:expiryStats.MissedFreeVersTasks", - "cmd:cmd:method:expiryStats.MissedTasks", - "cmd:cmd:method:expiryStats.MissedTierJournalTasks", - "cmd:cmd:method:expiryStats.NumWorkers", - "cmd:cmd:method:expiryTask.OpHash", - "cmd:cmd:method:firstByteRecorder.Read", - "cmd:cmd:method:format.String", - "cmd:cmd:method:formatErasureV3.Clone", - "cmd:cmd:method:formatErasureV3.Drives", - "cmd:cmd:method:forwardForTransport.RoundTrip", - "cmd:cmd:method:freeVersionTask.OpHash", - "cmd:cmd:method:ftpDriver.CheckPasswd", - "cmd:cmd:method:ftpDriver.DeleteDir", - "cmd:cmd:method:ftpDriver.DeleteFile", - "cmd:cmd:method:ftpDriver.GetFile", - "cmd:cmd:method:ftpDriver.ListDir", - "cmd:cmd:method:ftpDriver.MakeDir", - "cmd:cmd:method:ftpDriver.PutFile", - "cmd:cmd:method:ftpDriver.Rename", - "cmd:cmd:method:ftpDriver.Stat", - "cmd:cmd:method:guardedStorage.AppendFile", - "cmd:cmd:method:guardedStorage.CheckParts", - "cmd:cmd:method:guardedStorage.CleanAbandonedData", - "cmd:cmd:method:guardedStorage.CreateFile", - "cmd:cmd:method:guardedStorage.Delete", - "cmd:cmd:method:guardedStorage.DeleteBulk", - "cmd:cmd:method:guardedStorage.DeleteVersion", - "cmd:cmd:method:guardedStorage.DeleteVersions", - "cmd:cmd:method:guardedStorage.ListDir", - "cmd:cmd:method:guardedStorage.NSScanner", - "cmd:cmd:method:guardedStorage.ReadAll", - "cmd:cmd:method:guardedStorage.ReadFile", - "cmd:cmd:method:guardedStorage.ReadFileStream", - "cmd:cmd:method:guardedStorage.ReadParts", - "cmd:cmd:method:guardedStorage.ReadVersion", - "cmd:cmd:method:guardedStorage.ReadXL", - "cmd:cmd:method:guardedStorage.RenameData", - "cmd:cmd:method:guardedStorage.RenameFile", - "cmd:cmd:method:guardedStorage.RenamePart", - "cmd:cmd:method:guardedStorage.StatInfoFile", - "cmd:cmd:method:guardedStorage.UpdateMetadata", - "cmd:cmd:method:guardedStorage.VerifyFile", - "cmd:cmd:method:guardedStorage.WalkDir", - "cmd:cmd:method:guardedStorage.WriteAll", - "cmd:cmd:method:guardedStorage.WriteMetadata", - "cmd:cmd:method:hFlag.Has", - "cmd:cmd:method:healRoutine.AddWorker", - "cmd:cmd:method:healingMetric.String", - "cmd:cmd:method:healingTracker.DecodeMsg", - "cmd:cmd:method:healingTracker.EncodeMsg", - "cmd:cmd:method:healingTracker.MarshalMsg", - "cmd:cmd:method:healingTracker.Msgsize", - "cmd:cmd:method:healingTracker.UnmarshalMsg", - "cmd:cmd:method:importMetaReport.SetStatus", - "cmd:cmd:method:jentry.OpHash", - "cmd:cmd:method:kmsAPIHandlers.KMSAPIsHandler", - "cmd:cmd:method:kmsAPIHandlers.KMSCreateKeyHandler", - "cmd:cmd:method:kmsAPIHandlers.KMSKeyStatusHandler", - "cmd:cmd:method:kmsAPIHandlers.KMSListKeysHandler", - "cmd:cmd:method:kmsAPIHandlers.KMSMetricsHandler", - "cmd:cmd:method:kmsAPIHandlers.KMSStatusHandler", - "cmd:cmd:method:kmsAPIHandlers.KMSVersionHandler", - "cmd:cmd:method:lastDayTierStats.DecodeMsg", - "cmd:cmd:method:lastDayTierStats.EncodeMsg", - "cmd:cmd:method:lastDayTierStats.MarshalMsg", - "cmd:cmd:method:lastDayTierStats.Msgsize", - "cmd:cmd:method:lastDayTierStats.UnmarshalMsg", - "cmd:cmd:method:lastMinuteLatency.DecodeMsg", - "cmd:cmd:method:lastMinuteLatency.EncodeMsg", - "cmd:cmd:method:lastMinuteLatency.MarshalMsg", - "cmd:cmd:method:lastMinuteLatency.Msgsize", - "cmd:cmd:method:lastMinuteLatency.UnmarshalMsg", - "cmd:cmd:method:lcAuditEvent.Tags", - "cmd:cmd:method:lcEventSrc.String", - "cmd:cmd:method:listPathOptions.DecodeMsg", - "cmd:cmd:method:listPathOptions.EncodeMsg", - "cmd:cmd:method:listPathOptions.MarshalMsg", - "cmd:cmd:method:listPathOptions.Msgsize", - "cmd:cmd:method:listPathOptions.SetFilter", - "cmd:cmd:method:listPathOptions.UnmarshalMsg", - "cmd:cmd:method:listerAt.ListAt", - "cmd:cmd:method:localLockInstance.GetLock", - "cmd:cmd:method:localLockInstance.GetRLock", - "cmd:cmd:method:localLockInstance.RUnlock", - "cmd:cmd:method:localLockInstance.Unlock", - "cmd:cmd:method:localLockMap.DecodeMsg", - "cmd:cmd:method:localLockMap.EncodeMsg", - "cmd:cmd:method:localLockMap.MarshalMsg", - "cmd:cmd:method:localLockMap.Msgsize", - "cmd:cmd:method:localLockMap.UnmarshalMsg", - "cmd:cmd:method:localLocker.Close", - "cmd:cmd:method:localLocker.DupLockMap", - "cmd:cmd:method:localLocker.ForceUnlock", - "cmd:cmd:method:localLocker.IsLocal", - "cmd:cmd:method:localLocker.IsOnline", - "cmd:cmd:method:localLocker.Lock", - "cmd:cmd:method:localLocker.RLock", - "cmd:cmd:method:localLocker.RUnlock", - "cmd:cmd:method:localLocker.Refresh", - "cmd:cmd:method:localLocker.String", - "cmd:cmd:method:localLocker.Unlock", - "cmd:cmd:method:localPeerS3Client.DeleteBucket", - "cmd:cmd:method:localPeerS3Client.GetBucketInfo", - "cmd:cmd:method:localPeerS3Client.GetHost", - "cmd:cmd:method:localPeerS3Client.GetPools", - "cmd:cmd:method:localPeerS3Client.HealBucket", - "cmd:cmd:method:localPeerS3Client.ListBuckets", - "cmd:cmd:method:localPeerS3Client.MakeBucket", - "cmd:cmd:method:localPeerS3Client.SetPools", - "cmd:cmd:method:lockRESTClient.Close", - "cmd:cmd:method:lockRESTClient.ForceUnlock", - "cmd:cmd:method:lockRESTClient.IsLocal", - "cmd:cmd:method:lockRESTClient.IsOnline", - "cmd:cmd:method:lockRESTClient.Lock", - "cmd:cmd:method:lockRESTClient.RLock", - "cmd:cmd:method:lockRESTClient.RUnlock", - "cmd:cmd:method:lockRESTClient.Refresh", - "cmd:cmd:method:lockRESTClient.String", - "cmd:cmd:method:lockRESTClient.Unlock", - "cmd:cmd:method:lockRESTServer.ForceUnlockHandler", - "cmd:cmd:method:lockRESTServer.LockHandler", - "cmd:cmd:method:lockRESTServer.RLockHandler", - "cmd:cmd:method:lockRESTServer.RUnlockHandler", - "cmd:cmd:method:lockRESTServer.RefreshHandler", - "cmd:cmd:method:lockRESTServer.UnlockHandler", - "cmd:cmd:method:lockRequesterInfo.DecodeMsg", - "cmd:cmd:method:lockRequesterInfo.EncodeMsg", - "cmd:cmd:method:lockRequesterInfo.MarshalMsg", - "cmd:cmd:method:lockRequesterInfo.Msgsize", - "cmd:cmd:method:lockRequesterInfo.UnmarshalMsg", - "cmd:cmd:method:lockStats.DecodeMsg", - "cmd:cmd:method:lockStats.EncodeMsg", - "cmd:cmd:method:lockStats.MarshalMsg", - "cmd:cmd:method:lockStats.Msgsize", - "cmd:cmd:method:lockStats.UnmarshalMsg", - "cmd:cmd:method:metacache.DecodeMsg", - "cmd:cmd:method:metacache.EncodeMsg", - "cmd:cmd:method:metacache.MarshalMsg", - "cmd:cmd:method:metacache.Msgsize", - "cmd:cmd:method:metacache.UnmarshalMsg", - "cmd:cmd:method:metacacheBlockWriter.Close", - "cmd:cmd:method:metacacheReader.Close", - "cmd:cmd:method:metacacheWriter.Close", - "cmd:cmd:method:metacacheWriter.Reset", - "cmd:cmd:method:metricDisplay.String", - "cmd:cmd:method:metricDisplay.TableRow", - "cmd:cmd:method:metricsV3Server.ServeHTTP", - "cmd:cmd:method:minioBucketCollector.Collect", - "cmd:cmd:method:minioBucketCollector.Describe", - "cmd:cmd:method:minioClusterCollector.Collect", - "cmd:cmd:method:minioClusterCollector.Describe", - "cmd:cmd:method:minioCollector.Collect", - "cmd:cmd:method:minioCollector.Describe", - "cmd:cmd:method:minioFileInfo.IsDir", - "cmd:cmd:method:minioFileInfo.ModTime", - "cmd:cmd:method:minioFileInfo.Mode", - "cmd:cmd:method:minioFileInfo.Name", - "cmd:cmd:method:minioFileInfo.Size", - "cmd:cmd:method:minioFileInfo.Sys", - "cmd:cmd:method:minioLogger.Print", - "cmd:cmd:method:minioLogger.PrintCommand", - "cmd:cmd:method:minioLogger.PrintResponse", - "cmd:cmd:method:minioLogger.Printf", - "cmd:cmd:method:minioNodeCollector.Collect", - "cmd:cmd:method:minioNodeCollector.Describe", - "cmd:cmd:method:minioResourceCollector.Collect", - "cmd:cmd:method:minioResourceCollector.Describe", - "cmd:cmd:method:multiWriter.Write", - "cmd:cmd:method:mustReplicateOptions.ReplicationStatus", - "cmd:cmd:method:netPerfRX.ActiveConnections", - "cmd:cmd:method:netPerfRX.Connect", - "cmd:cmd:method:netPerfRX.Disconnect", - "cmd:cmd:method:netPerfRX.Reset", - "cmd:cmd:method:netperfReader.Read", - "cmd:cmd:method:noncurrentVersionsTask.OpHash", - "cmd:cmd:method:nsLockMap.NewNSLock", - "cmd:cmd:method:nsScannerOptions.DecodeMsg", - "cmd:cmd:method:nsScannerOptions.EncodeMsg", - "cmd:cmd:method:nsScannerOptions.MarshalMsg", - "cmd:cmd:method:nsScannerOptions.Msgsize", - "cmd:cmd:method:nsScannerOptions.UnmarshalMsg", - "cmd:cmd:method:nsScannerResp.DecodeMsg", - "cmd:cmd:method:nsScannerResp.EncodeMsg", - "cmd:cmd:method:nsScannerResp.MarshalMsg", - "cmd:cmd:method:nsScannerResp.Msgsize", - "cmd:cmd:method:nsScannerResp.UnmarshalMsg", - "cmd:cmd:method:objInfoCache.Add", - "cmd:cmd:method:objInfoCache.Get", - "cmd:cmd:method:objSweeper.GetOpts", - "cmd:cmd:method:objSweeper.SetTransitionState", - "cmd:cmd:method:objSweeper.Sweep", - "cmd:cmd:method:objSweeper.WithVersion", - "cmd:cmd:method:objSweeper.WithVersioning", - "cmd:cmd:method:objectAPIHandlers.AbortMultipartUploadHandler", - "cmd:cmd:method:objectAPIHandlers.CompleteMultipartUploadHandler", - "cmd:cmd:method:objectAPIHandlers.CopyObjectHandler", - "cmd:cmd:method:objectAPIHandlers.CopyObjectPartHandler", - "cmd:cmd:method:objectAPIHandlers.DeleteBucketCorsHandler", - "cmd:cmd:method:objectAPIHandlers.DeleteBucketEncryptionHandler", - "cmd:cmd:method:objectAPIHandlers.DeleteBucketHandler", - "cmd:cmd:method:objectAPIHandlers.DeleteBucketLifecycleHandler", - "cmd:cmd:method:objectAPIHandlers.DeleteBucketPolicyHandler", - "cmd:cmd:method:objectAPIHandlers.DeleteBucketReplicationConfigHandler", - "cmd:cmd:method:objectAPIHandlers.DeleteBucketTaggingHandler", - "cmd:cmd:method:objectAPIHandlers.DeleteBucketWebsiteHandler", - "cmd:cmd:method:objectAPIHandlers.DeleteMultipleObjectsHandler", - "cmd:cmd:method:objectAPIHandlers.DeleteObjectHandler", - "cmd:cmd:method:objectAPIHandlers.DeleteObjectTaggingHandler", - "cmd:cmd:method:objectAPIHandlers.GetBucketACLHandler", - "cmd:cmd:method:objectAPIHandlers.GetBucketAccelerateHandler", - "cmd:cmd:method:objectAPIHandlers.GetBucketCorsHandler", - "cmd:cmd:method:objectAPIHandlers.GetBucketEncryptionHandler", - "cmd:cmd:method:objectAPIHandlers.GetBucketLifecycleHandler", - "cmd:cmd:method:objectAPIHandlers.GetBucketLocationHandler", - "cmd:cmd:method:objectAPIHandlers.GetBucketLoggingHandler", - "cmd:cmd:method:objectAPIHandlers.GetBucketNotificationHandler", - "cmd:cmd:method:objectAPIHandlers.GetBucketObjectLockConfigHandler", - "cmd:cmd:method:objectAPIHandlers.GetBucketPolicyHandler", - "cmd:cmd:method:objectAPIHandlers.GetBucketPolicyStatusHandler", - "cmd:cmd:method:objectAPIHandlers.GetBucketReplicationConfigHandler", - "cmd:cmd:method:objectAPIHandlers.GetBucketReplicationMetricsHandler", - "cmd:cmd:method:objectAPIHandlers.GetBucketReplicationMetricsV2Handler", - "cmd:cmd:method:objectAPIHandlers.GetBucketRequestPaymentHandler", - "cmd:cmd:method:objectAPIHandlers.GetBucketTaggingHandler", - "cmd:cmd:method:objectAPIHandlers.GetBucketVersioningHandler", - "cmd:cmd:method:objectAPIHandlers.GetBucketWebsiteHandler", - "cmd:cmd:method:objectAPIHandlers.GetObjectACLHandler", - "cmd:cmd:method:objectAPIHandlers.GetObjectAttributesHandler", - "cmd:cmd:method:objectAPIHandlers.GetObjectHandler", - "cmd:cmd:method:objectAPIHandlers.GetObjectLambdaHandler", - "cmd:cmd:method:objectAPIHandlers.GetObjectLegalHoldHandler", - "cmd:cmd:method:objectAPIHandlers.GetObjectRetentionHandler", - "cmd:cmd:method:objectAPIHandlers.GetObjectTaggingHandler", - "cmd:cmd:method:objectAPIHandlers.HeadBucketHandler", - "cmd:cmd:method:objectAPIHandlers.HeadObjectHandler", - "cmd:cmd:method:objectAPIHandlers.ListBucketsHandler", - "cmd:cmd:method:objectAPIHandlers.ListMultipartUploadsHandler", - "cmd:cmd:method:objectAPIHandlers.ListObjectPartsHandler", - "cmd:cmd:method:objectAPIHandlers.ListObjectVersionsHandler", - "cmd:cmd:method:objectAPIHandlers.ListObjectVersionsMHandler", - "cmd:cmd:method:objectAPIHandlers.ListObjectsV1Handler", - "cmd:cmd:method:objectAPIHandlers.ListObjectsV2Handler", - "cmd:cmd:method:objectAPIHandlers.ListObjectsV2MHandler", - "cmd:cmd:method:objectAPIHandlers.ListenNotificationHandler", - "cmd:cmd:method:objectAPIHandlers.NewMultipartUploadHandler", - "cmd:cmd:method:objectAPIHandlers.PostPolicyBucketHandler", - "cmd:cmd:method:objectAPIHandlers.PostRestoreObjectHandler", - "cmd:cmd:method:objectAPIHandlers.PutBucketACLHandler", - "cmd:cmd:method:objectAPIHandlers.PutBucketCorsHandler", - "cmd:cmd:method:objectAPIHandlers.PutBucketEncryptionHandler", - "cmd:cmd:method:objectAPIHandlers.PutBucketHandler", - "cmd:cmd:method:objectAPIHandlers.PutBucketLifecycleHandler", - "cmd:cmd:method:objectAPIHandlers.PutBucketNotificationHandler", - "cmd:cmd:method:objectAPIHandlers.PutBucketObjectLockConfigHandler", - "cmd:cmd:method:objectAPIHandlers.PutBucketPolicyHandler", - "cmd:cmd:method:objectAPIHandlers.PutBucketReplicationConfigHandler", - "cmd:cmd:method:objectAPIHandlers.PutBucketTaggingHandler", - "cmd:cmd:method:objectAPIHandlers.PutBucketVersioningHandler", - "cmd:cmd:method:objectAPIHandlers.PutObjectACLHandler", - "cmd:cmd:method:objectAPIHandlers.PutObjectExtractHandler", - "cmd:cmd:method:objectAPIHandlers.PutObjectHandler", - "cmd:cmd:method:objectAPIHandlers.PutObjectLegalHoldHandler", - "cmd:cmd:method:objectAPIHandlers.PutObjectPartHandler", - "cmd:cmd:method:objectAPIHandlers.PutObjectRetentionHandler", - "cmd:cmd:method:objectAPIHandlers.PutObjectTaggingHandler", - "cmd:cmd:method:objectAPIHandlers.ResetBucketReplicationStartHandler", - "cmd:cmd:method:objectAPIHandlers.ResetBucketReplicationStatusHandler", - "cmd:cmd:method:objectAPIHandlers.SelectObjectContentHandler", - "cmd:cmd:method:objectAPIHandlers.ValidateBucketReplicationCredsHandler", - "cmd:cmd:method:osMetric.String", - "cmd:cmd:method:parallelReader.Done", - "cmd:cmd:method:parallelReader.Read", - "cmd:cmd:method:peerRESTClient.BackgroundHealStatus", - "cmd:cmd:method:peerRESTClient.Close", - "cmd:cmd:method:peerRESTClient.CommitBinary", - "cmd:cmd:method:peerRESTClient.ConsoleLog", - "cmd:cmd:method:peerRESTClient.DeleteBucketMetadata", - "cmd:cmd:method:peerRESTClient.DeletePolicy", - "cmd:cmd:method:peerRESTClient.DeleteServiceAccount", - "cmd:cmd:method:peerRESTClient.DeleteUploadID", - "cmd:cmd:method:peerRESTClient.DeleteUser", - "cmd:cmd:method:peerRESTClient.DevNull", - "cmd:cmd:method:peerRESTClient.DownloadProfileData", - "cmd:cmd:method:peerRESTClient.DriveSpeedTest", - "cmd:cmd:method:peerRESTClient.GetAllBucketStats", - "cmd:cmd:method:peerRESTClient.GetBucketStats", - "cmd:cmd:method:peerRESTClient.GetCPUs", - "cmd:cmd:method:peerRESTClient.GetLastDayTierStats", - "cmd:cmd:method:peerRESTClient.GetLocks", - "cmd:cmd:method:peerRESTClient.GetMemInfo", - "cmd:cmd:method:peerRESTClient.GetMetacacheListing", - "cmd:cmd:method:peerRESTClient.GetMetrics", - "cmd:cmd:method:peerRESTClient.GetNetInfo", - "cmd:cmd:method:peerRESTClient.GetOSInfo", - "cmd:cmd:method:peerRESTClient.GetPartitions", - "cmd:cmd:method:peerRESTClient.GetPeerBucketMetrics", - "cmd:cmd:method:peerRESTClient.GetPeerMetrics", - "cmd:cmd:method:peerRESTClient.GetProcInfo", - "cmd:cmd:method:peerRESTClient.GetReplicationMRF", - "cmd:cmd:method:peerRESTClient.GetResourceMetrics", - "cmd:cmd:method:peerRESTClient.GetSELinuxInfo", - "cmd:cmd:method:peerRESTClient.GetSRMetrics", - "cmd:cmd:method:peerRESTClient.GetSysConfig", - "cmd:cmd:method:peerRESTClient.GetSysErrors", - "cmd:cmd:method:peerRESTClient.IsOnline", - "cmd:cmd:method:peerRESTClient.Listen", - "cmd:cmd:method:peerRESTClient.LoadBucketMetadata", - "cmd:cmd:method:peerRESTClient.LoadGroup", - "cmd:cmd:method:peerRESTClient.LoadPolicy", - "cmd:cmd:method:peerRESTClient.LoadPolicyMapping", - "cmd:cmd:method:peerRESTClient.LoadRebalanceMeta", - "cmd:cmd:method:peerRESTClient.LoadServiceAccount", - "cmd:cmd:method:peerRESTClient.LoadTransitionTierConfig", - "cmd:cmd:method:peerRESTClient.LoadUser", - "cmd:cmd:method:peerRESTClient.LocalStorageInfo", - "cmd:cmd:method:peerRESTClient.MonitorBandwidth", - "cmd:cmd:method:peerRESTClient.Netperf", - "cmd:cmd:method:peerRESTClient.ReloadPoolMeta", - "cmd:cmd:method:peerRESTClient.ReloadSiteReplicationConfig", - "cmd:cmd:method:peerRESTClient.ServerInfo", - "cmd:cmd:method:peerRESTClient.SignalService", - "cmd:cmd:method:peerRESTClient.SpeedTest", - "cmd:cmd:method:peerRESTClient.StartProfiling", - "cmd:cmd:method:peerRESTClient.StopRebalance", - "cmd:cmd:method:peerRESTClient.String", - "cmd:cmd:method:peerRESTClient.Trace", - "cmd:cmd:method:peerRESTClient.UpdateMetacacheListing", - "cmd:cmd:method:peerRESTClient.VerifyBinary", - "cmd:cmd:method:peerRESTServer.BackgroundHealStatusHandler", - "cmd:cmd:method:peerRESTServer.CommitBinaryHandler", - "cmd:cmd:method:peerRESTServer.ConsoleLogHandler", - "cmd:cmd:method:peerRESTServer.DeleteBucketHandler", - "cmd:cmd:method:peerRESTServer.DeleteBucketMetadataHandler", - "cmd:cmd:method:peerRESTServer.DeletePolicyHandler", - "cmd:cmd:method:peerRESTServer.DeleteServiceAccountHandler", - "cmd:cmd:method:peerRESTServer.DeleteUserHandler", - "cmd:cmd:method:peerRESTServer.DevNull", - "cmd:cmd:method:peerRESTServer.DownloadProfilingDataHandler", - "cmd:cmd:method:peerRESTServer.DriveSpeedTestHandler", - "cmd:cmd:method:peerRESTServer.GetAllBucketStatsHandler", - "cmd:cmd:method:peerRESTServer.GetBandwidth", - "cmd:cmd:method:peerRESTServer.GetBucketStatsHandler", - "cmd:cmd:method:peerRESTServer.GetCPUsHandler", - "cmd:cmd:method:peerRESTServer.GetLastDayTierStatsHandler", - "cmd:cmd:method:peerRESTServer.GetLocksHandler", - "cmd:cmd:method:peerRESTServer.GetMemInfoHandler", - "cmd:cmd:method:peerRESTServer.GetMetacacheListingHandler", - "cmd:cmd:method:peerRESTServer.GetMetricsHandler", - "cmd:cmd:method:peerRESTServer.GetNetInfoHandler", - "cmd:cmd:method:peerRESTServer.GetOSInfoHandler", - "cmd:cmd:method:peerRESTServer.GetPartitionsHandler", - "cmd:cmd:method:peerRESTServer.GetPeerBucketMetrics", - "cmd:cmd:method:peerRESTServer.GetPeerMetrics", - "cmd:cmd:method:peerRESTServer.GetProcInfoHandler", - "cmd:cmd:method:peerRESTServer.GetReplicationMRFHandler", - "cmd:cmd:method:peerRESTServer.GetResourceMetrics", - "cmd:cmd:method:peerRESTServer.GetSRMetricsHandler", - "cmd:cmd:method:peerRESTServer.GetSysConfigHandler", - "cmd:cmd:method:peerRESTServer.GetSysErrorsHandler", - "cmd:cmd:method:peerRESTServer.GetSysServicesHandler", - "cmd:cmd:method:peerRESTServer.HandlerClearUploadID", - "cmd:cmd:method:peerRESTServer.HeadBucketHandler", - "cmd:cmd:method:peerRESTServer.HealBucketHandler", - "cmd:cmd:method:peerRESTServer.HealthHandler", - "cmd:cmd:method:peerRESTServer.IsValid", - "cmd:cmd:method:peerRESTServer.ListBucketsHandler", - "cmd:cmd:method:peerRESTServer.ListenHandler", - "cmd:cmd:method:peerRESTServer.LoadBucketMetadataHandler", - "cmd:cmd:method:peerRESTServer.LoadGroupHandler", - "cmd:cmd:method:peerRESTServer.LoadPolicyHandler", - "cmd:cmd:method:peerRESTServer.LoadPolicyMappingHandler", - "cmd:cmd:method:peerRESTServer.LoadRebalanceMetaHandler", - "cmd:cmd:method:peerRESTServer.LoadServiceAccountHandler", - "cmd:cmd:method:peerRESTServer.LoadTransitionTierConfigHandler", - "cmd:cmd:method:peerRESTServer.LoadUserHandler", - "cmd:cmd:method:peerRESTServer.LocalStorageInfoHandler", - "cmd:cmd:method:peerRESTServer.MakeBucketHandler", - "cmd:cmd:method:peerRESTServer.NetSpeedTestHandler", - "cmd:cmd:method:peerRESTServer.PutBucketNotificationHandler", - "cmd:cmd:method:peerRESTServer.ReloadPoolMetaHandler", - "cmd:cmd:method:peerRESTServer.ReloadSiteReplicationConfigHandler", - "cmd:cmd:method:peerRESTServer.ServerInfoHandler", - "cmd:cmd:method:peerRESTServer.SignalServiceHandler", - "cmd:cmd:method:peerRESTServer.SpeedTestHandler", - "cmd:cmd:method:peerRESTServer.StartProfilingHandler", - "cmd:cmd:method:peerRESTServer.StopRebalanceHandler", - "cmd:cmd:method:peerRESTServer.TraceHandler", - "cmd:cmd:method:peerRESTServer.UpdateMetacacheListingHandler", - "cmd:cmd:method:peerRESTServer.VerifyBinaryHandler", - "cmd:cmd:method:poolMeta.BucketDone", - "cmd:cmd:method:poolMeta.CountItem", - "cmd:cmd:method:poolMeta.DecodeMsg", - "cmd:cmd:method:poolMeta.Decommission", - "cmd:cmd:method:poolMeta.DecommissionCancel", - "cmd:cmd:method:poolMeta.DecommissionComplete", - "cmd:cmd:method:poolMeta.DecommissionFailed", - "cmd:cmd:method:poolMeta.EncodeMsg", - "cmd:cmd:method:poolMeta.IsSuspended", - "cmd:cmd:method:poolMeta.MarshalMsg", - "cmd:cmd:method:poolMeta.Msgsize", - "cmd:cmd:method:poolMeta.PendingBuckets", - "cmd:cmd:method:poolMeta.QueueBuckets", - "cmd:cmd:method:poolMeta.ResumeBucketObject", - "cmd:cmd:method:poolMeta.TrackCurrentBucketObject", - "cmd:cmd:method:poolMeta.UnmarshalMsg", - "cmd:cmd:method:poolSpaceInfo.DecodeMsg", - "cmd:cmd:method:poolSpaceInfo.EncodeMsg", - "cmd:cmd:method:poolSpaceInfo.MarshalMsg", - "cmd:cmd:method:poolSpaceInfo.Msgsize", - "cmd:cmd:method:poolSpaceInfo.UnmarshalMsg", - "cmd:cmd:method:profilerWrapper.Extension", - "cmd:cmd:method:profilerWrapper.Records", - "cmd:cmd:method:profilerWrapper.Stop", - "cmd:cmd:method:promLogger.Println", - "cmd:cmd:method:rebalSaveOpts.DecodeMsg", - "cmd:cmd:method:rebalSaveOpts.EncodeMsg", - "cmd:cmd:method:rebalSaveOpts.MarshalMsg", - "cmd:cmd:method:rebalSaveOpts.Msgsize", - "cmd:cmd:method:rebalSaveOpts.UnmarshalMsg", - "cmd:cmd:method:rebalStatus.DecodeMsg", - "cmd:cmd:method:rebalStatus.EncodeMsg", - "cmd:cmd:method:rebalStatus.MarshalMsg", - "cmd:cmd:method:rebalStatus.Msgsize", - "cmd:cmd:method:rebalStatus.String", - "cmd:cmd:method:rebalStatus.UnmarshalMsg", - "cmd:cmd:method:rebalanceInfo.DecodeMsg", - "cmd:cmd:method:rebalanceInfo.EncodeMsg", - "cmd:cmd:method:rebalanceInfo.MarshalMsg", - "cmd:cmd:method:rebalanceInfo.Msgsize", - "cmd:cmd:method:rebalanceInfo.UnmarshalMsg", - "cmd:cmd:method:rebalanceMeta.DecodeMsg", - "cmd:cmd:method:rebalanceMeta.EncodeMsg", - "cmd:cmd:method:rebalanceMeta.MarshalMsg", - "cmd:cmd:method:rebalanceMeta.Msgsize", - "cmd:cmd:method:rebalanceMeta.UnmarshalMsg", - "cmd:cmd:method:rebalanceMetric.DecodeMsg", - "cmd:cmd:method:rebalanceMetric.EncodeMsg", - "cmd:cmd:method:rebalanceMetric.MarshalMsg", - "cmd:cmd:method:rebalanceMetric.Msgsize", - "cmd:cmd:method:rebalanceMetric.String", - "cmd:cmd:method:rebalanceMetric.UnmarshalMsg", - "cmd:cmd:method:rebalanceMetrics.DecodeMsg", - "cmd:cmd:method:rebalanceMetrics.EncodeMsg", - "cmd:cmd:method:rebalanceMetrics.MarshalMsg", - "cmd:cmd:method:rebalanceMetrics.Msgsize", - "cmd:cmd:method:rebalanceMetrics.UnmarshalMsg", - "cmd:cmd:method:rebalanceStats.DecodeMsg", - "cmd:cmd:method:rebalanceStats.EncodeMsg", - "cmd:cmd:method:rebalanceStats.MarshalMsg", - "cmd:cmd:method:rebalanceStats.Msgsize", - "cmd:cmd:method:rebalanceStats.UnmarshalMsg", - "cmd:cmd:method:remotePeerS3Client.DeleteBucket", - "cmd:cmd:method:remotePeerS3Client.GetBucketInfo", - "cmd:cmd:method:remotePeerS3Client.GetHost", - "cmd:cmd:method:remotePeerS3Client.GetPools", - "cmd:cmd:method:remotePeerS3Client.HealBucket", - "cmd:cmd:method:remotePeerS3Client.ListBuckets", - "cmd:cmd:method:remotePeerS3Client.MakeBucket", - "cmd:cmd:method:remotePeerS3Client.SetPools", - "cmd:cmd:method:replicateTargetDecision.String", - "cmd:cmd:method:replicatedInfos.Action", - "cmd:cmd:method:replicatedInfos.CompletedSize", - "cmd:cmd:method:replicatedInfos.ReplicationResynced", - "cmd:cmd:method:replicatedInfos.ReplicationStatus", - "cmd:cmd:method:replicatedInfos.ReplicationStatusInternal", - "cmd:cmd:method:replicatedInfos.VersionPurgeStatus", - "cmd:cmd:method:replicatedInfos.VersionPurgeStatusInternal", - "cmd:cmd:method:replicatedTargetInfo.Empty", - "cmd:cmd:method:replicationConfig.Empty", - "cmd:cmd:method:replicationConfig.Replicate", - "cmd:cmd:method:replicationConfig.Resync", - "cmd:cmd:method:replicationResyncer.PersistToDisk", - "cmd:cmd:method:restoreObjStatus.Expiry", - "cmd:cmd:method:restoreObjStatus.OnDisk", - "cmd:cmd:method:restoreObjStatus.Ongoing", - "cmd:cmd:method:restoreObjStatus.String", - "cmd:cmd:method:rstats.DecodeMsg", - "cmd:cmd:method:rstats.EncodeMsg", - "cmd:cmd:method:rstats.MarshalMsg", - "cmd:cmd:method:rstats.Msgsize", - "cmd:cmd:method:rstats.UnmarshalMsg", - "cmd:cmd:method:s3ChunkedReader.Close", - "cmd:cmd:method:s3ChunkedReader.Read", - "cmd:cmd:method:s3UnsignedChunkedReader.Close", - "cmd:cmd:method:s3UnsignedChunkedReader.Read", - "cmd:cmd:method:scanStatus.DecodeMsg", - "cmd:cmd:method:scanStatus.EncodeMsg", - "cmd:cmd:method:scanStatus.MarshalMsg", - "cmd:cmd:method:scanStatus.Msgsize", - "cmd:cmd:method:scanStatus.UnmarshalMsg", - "cmd:cmd:method:scannerMetric.String", - "cmd:cmd:method:serverPoolsAvailableSpace.FilterMaxUsed", - "cmd:cmd:method:serverPoolsAvailableSpace.TotalAvailable", - "cmd:cmd:method:sftpDriver.AccessKey", - "cmd:cmd:method:sftpDriver.Filecmd", - "cmd:cmd:method:sftpDriver.Filelist", - "cmd:cmd:method:sftpDriver.Fileread", - "cmd:cmd:method:sftpDriver.Filewrite", - "cmd:cmd:method:sftpLogger.Error", - "cmd:cmd:method:sftpLogger.Info", - "cmd:cmd:method:sharedLock.GetLock", - "cmd:cmd:method:siteReplicatorCred.Get", - "cmd:cmd:method:siteReplicatorCred.IsValid", - "cmd:cmd:method:siteReplicatorCred.Set", - "cmd:cmd:method:sizeHistogram.DecodeMsg", - "cmd:cmd:method:sizeHistogram.EncodeMsg", - "cmd:cmd:method:sizeHistogram.MarshalMsg", - "cmd:cmd:method:sizeHistogram.Msgsize", - "cmd:cmd:method:sizeHistogram.UnmarshalMsg", - "cmd:cmd:method:sizeHistogramV1.DecodeMsg", - "cmd:cmd:method:sizeHistogramV1.EncodeMsg", - "cmd:cmd:method:sizeHistogramV1.MarshalMsg", - "cmd:cmd:method:sizeHistogramV1.Msgsize", - "cmd:cmd:method:sizeHistogramV1.UnmarshalMsg", - "cmd:cmd:method:storageMetric.String", - "cmd:cmd:method:storageRESTClient.AppendFile", - "cmd:cmd:method:storageRESTClient.CheckParts", - "cmd:cmd:method:storageRESTClient.CleanAbandonedData", - "cmd:cmd:method:storageRESTClient.Close", - "cmd:cmd:method:storageRESTClient.CreateFile", - "cmd:cmd:method:storageRESTClient.Delete", - "cmd:cmd:method:storageRESTClient.DeleteBulk", - "cmd:cmd:method:storageRESTClient.DeleteVersion", - "cmd:cmd:method:storageRESTClient.DeleteVersions", - "cmd:cmd:method:storageRESTClient.DeleteVol", - "cmd:cmd:method:storageRESTClient.DiskInfo", - "cmd:cmd:method:storageRESTClient.Endpoint", - "cmd:cmd:method:storageRESTClient.GetDiskID", - "cmd:cmd:method:storageRESTClient.GetDiskLoc", - "cmd:cmd:method:storageRESTClient.Healing", - "cmd:cmd:method:storageRESTClient.Hostname", - "cmd:cmd:method:storageRESTClient.IsLocal", - "cmd:cmd:method:storageRESTClient.IsOnline", - "cmd:cmd:method:storageRESTClient.IsOnlineWS", - "cmd:cmd:method:storageRESTClient.LastConn", - "cmd:cmd:method:storageRESTClient.ListDir", - "cmd:cmd:method:storageRESTClient.ListVols", - "cmd:cmd:method:storageRESTClient.MakeVol", - "cmd:cmd:method:storageRESTClient.MakeVolBulk", - "cmd:cmd:method:storageRESTClient.NSScanner", - "cmd:cmd:method:storageRESTClient.ReadAll", - "cmd:cmd:method:storageRESTClient.ReadFile", - "cmd:cmd:method:storageRESTClient.ReadFileStream", - "cmd:cmd:method:storageRESTClient.ReadParts", - "cmd:cmd:method:storageRESTClient.ReadVersion", - "cmd:cmd:method:storageRESTClient.ReadXL", - "cmd:cmd:method:storageRESTClient.RenameData", - "cmd:cmd:method:storageRESTClient.RenameFile", - "cmd:cmd:method:storageRESTClient.RenamePart", - "cmd:cmd:method:storageRESTClient.SetDiskID", - "cmd:cmd:method:storageRESTClient.StatInfoFile", - "cmd:cmd:method:storageRESTClient.StatVol", - "cmd:cmd:method:storageRESTClient.String", - "cmd:cmd:method:storageRESTClient.UpdateMetadata", - "cmd:cmd:method:storageRESTClient.VerifyFile", - "cmd:cmd:method:storageRESTClient.WalkDir", - "cmd:cmd:method:storageRESTClient.WriteAll", - "cmd:cmd:method:storageRESTClient.WriteMetadata", - "cmd:cmd:method:storageRESTServer.AppendFileHandler", - "cmd:cmd:method:storageRESTServer.CheckPartsHandler", - "cmd:cmd:method:storageRESTServer.CleanAbandonedDataHandler", - "cmd:cmd:method:storageRESTServer.CreateFileHandler", - "cmd:cmd:method:storageRESTServer.DeleteBulkHandler", - "cmd:cmd:method:storageRESTServer.DeleteFileHandler", - "cmd:cmd:method:storageRESTServer.DeleteVersionHandler", - "cmd:cmd:method:storageRESTServer.DeleteVersionsHandler", - "cmd:cmd:method:storageRESTServer.DiskInfoHandler", - "cmd:cmd:method:storageRESTServer.HealthHandler", - "cmd:cmd:method:storageRESTServer.IsAuthValid", - "cmd:cmd:method:storageRESTServer.IsValid", - "cmd:cmd:method:storageRESTServer.ListDirHandler", - "cmd:cmd:method:storageRESTServer.MakeVolBulkHandler", - "cmd:cmd:method:storageRESTServer.MakeVolHandler", - "cmd:cmd:method:storageRESTServer.NSScannerHandler", - "cmd:cmd:method:storageRESTServer.ReadAllHandler", - "cmd:cmd:method:storageRESTServer.ReadFileHandler", - "cmd:cmd:method:storageRESTServer.ReadFileStreamHandler", - "cmd:cmd:method:storageRESTServer.ReadPartsHandler", - "cmd:cmd:method:storageRESTServer.ReadVersionHandler", - "cmd:cmd:method:storageRESTServer.ReadVersionHandlerWS", - "cmd:cmd:method:storageRESTServer.ReadXLHandler", - "cmd:cmd:method:storageRESTServer.ReadXLHandlerWS", - "cmd:cmd:method:storageRESTServer.RenameDataHandler", - "cmd:cmd:method:storageRESTServer.RenameDataInlineHandler", - "cmd:cmd:method:storageRESTServer.RenameFileHandler", - "cmd:cmd:method:storageRESTServer.RenamePartHandler", - "cmd:cmd:method:storageRESTServer.StatInfoFile", - "cmd:cmd:method:storageRESTServer.StatVolHandler", - "cmd:cmd:method:storageRESTServer.UpdateMetadataHandler", - "cmd:cmd:method:storageRESTServer.VerifyFileHandler", - "cmd:cmd:method:storageRESTServer.WalkDirHandler", - "cmd:cmd:method:storageRESTServer.WriteAllHandler", - "cmd:cmd:method:storageRESTServer.WriteMetadataHandler", - "cmd:cmd:method:streamingBitrotReader.Close", - "cmd:cmd:method:streamingBitrotReader.ReadAt", - "cmd:cmd:method:streamingBitrotWriter.Close", - "cmd:cmd:method:streamingBitrotWriter.Write", - "cmd:cmd:method:stsAPIHandlers.AssumeRole", - "cmd:cmd:method:stsAPIHandlers.AssumeRoleWithCertificate", - "cmd:cmd:method:stsAPIHandlers.AssumeRoleWithClientGrants", - "cmd:cmd:method:stsAPIHandlers.AssumeRoleWithCustomToken", - "cmd:cmd:method:stsAPIHandlers.AssumeRoleWithLDAPIdentity", - "cmd:cmd:method:stsAPIHandlers.AssumeRoleWithSSO", - "cmd:cmd:method:stsAPIHandlers.AssumeRoleWithWebIdentity", - "cmd:cmd:method:stsErrorCodeMap.ToSTSErr", - "cmd:cmd:method:stsLDAPLoginKeyLimiterSet.Allow", - "cmd:cmd:method:stsLDAPLoginKeyLimiterSet.Reserve", - "cmd:cmd:method:stsLDAPLoginKeyReservation.CancelAt", - "cmd:cmd:method:stsLDAPLoginKeyReservation.CommitAt", - "cmd:cmd:method:stsLDAPLoginRateLimiter.Allow", - "cmd:cmd:method:stsLDAPLoginRateLimiter.Reserve", - "cmd:cmd:method:stsLDAPLoginReservation.Cancel", - "cmd:cmd:method:stsLDAPLoginReservation.Commit", - "cmd:cmd:method:tierMetrics.Observe", - "cmd:cmd:method:tierMetrics.Report", - "cmd:cmd:method:tierOp.String", - "cmd:cmd:method:tierPermErr.Error", - "cmd:cmd:method:tierStats.DecodeMsg", - "cmd:cmd:method:tierStats.EncodeMsg", - "cmd:cmd:method:tierStats.MarshalMsg", - "cmd:cmd:method:tierStats.Msgsize", - "cmd:cmd:method:tierStats.UnmarshalMsg", - "cmd:cmd:method:trackingResponseWriter.Flush", - "cmd:cmd:method:trackingResponseWriter.Unwrap", - "cmd:cmd:method:trackingResponseWriter.Write", - "cmd:cmd:method:trackingResponseWriter.WriteHeader", - "cmd:cmd:method:transitionState.ActiveTasks", - "cmd:cmd:method:transitionState.Init", - "cmd:cmd:method:transitionState.MissedImmediateTasks", - "cmd:cmd:method:transitionState.PendingTasks", - "cmd:cmd:method:transitionState.UpdateWorkers", - "cmd:cmd:method:versionsHistogram.DecodeMsg", - "cmd:cmd:method:versionsHistogram.EncodeMsg", - "cmd:cmd:method:versionsHistogram.MarshalMsg", - "cmd:cmd:method:versionsHistogram.Msgsize", - "cmd:cmd:method:versionsHistogram.UnmarshalMsg", - "cmd:cmd:method:warmBackendAzure.Get", - "cmd:cmd:method:warmBackendAzure.InUse", - "cmd:cmd:method:warmBackendAzure.Put", - "cmd:cmd:method:warmBackendAzure.PutWithMeta", - "cmd:cmd:method:warmBackendAzure.Remove", - "cmd:cmd:method:warmBackendGCS.Get", - "cmd:cmd:method:warmBackendGCS.InUse", - "cmd:cmd:method:warmBackendGCS.Put", - "cmd:cmd:method:warmBackendGCS.PutWithMeta", - "cmd:cmd:method:warmBackendGCS.Remove", - "cmd:cmd:method:warmBackendMinIO.Put", - "cmd:cmd:method:warmBackendMinIO.PutWithMeta", - "cmd:cmd:method:warmBackendS3.Get", - "cmd:cmd:method:warmBackendS3.InUse", - "cmd:cmd:method:warmBackendS3.Put", - "cmd:cmd:method:warmBackendS3.PutWithMeta", - "cmd:cmd:method:warmBackendS3.Remove", - "cmd:cmd:method:warmBackendS3.ToObjectError", - "cmd:cmd:method:wholeBitrotReader.ReadAt", - "cmd:cmd:method:wholeBitrotWriter.Close", - "cmd:cmd:method:wholeBitrotWriter.Write", - "cmd:cmd:method:writerAt.Close", - "cmd:cmd:method:writerAt.TransferError", - "cmd:cmd:method:writerAt.WriteAt", - "cmd:cmd:method:xlFlags.DecodeMsg", - "cmd:cmd:method:xlFlags.EncodeMsg", - "cmd:cmd:method:xlFlags.MarshalMsg", - "cmd:cmd:method:xlFlags.Msgsize", - "cmd:cmd:method:xlFlags.String", - "cmd:cmd:method:xlFlags.UnmarshalMsg", - "cmd:cmd:method:xlMetaBuf.AllHidden", - "cmd:cmd:method:xlMetaBuf.DecodeMsg", - "cmd:cmd:method:xlMetaBuf.EncodeMsg", - "cmd:cmd:method:xlMetaBuf.IsLatestDeleteMarker", - "cmd:cmd:method:xlMetaBuf.ListVersions", - "cmd:cmd:method:xlMetaBuf.MarshalMsg", - "cmd:cmd:method:xlMetaBuf.Msgsize", - "cmd:cmd:method:xlMetaBuf.ToFileInfo", - "cmd:cmd:method:xlMetaBuf.UnmarshalMsg", - "cmd:cmd:method:xlMetaDataDirDecoder.DecodeMsg", - "cmd:cmd:method:xlMetaDataDirDecoder.EncodeMsg", - "cmd:cmd:method:xlMetaDataDirDecoder.MarshalMsg", - "cmd:cmd:method:xlMetaDataDirDecoder.Msgsize", - "cmd:cmd:method:xlMetaDataDirDecoder.UnmarshalMsg", - "cmd:cmd:method:xlMetaV1Object.DecodeMsg", - "cmd:cmd:method:xlMetaV1Object.EncodeMsg", - "cmd:cmd:method:xlMetaV1Object.MarshalMsg", - "cmd:cmd:method:xlMetaV1Object.Msgsize", - "cmd:cmd:method:xlMetaV1Object.Signature", - "cmd:cmd:method:xlMetaV1Object.ToFileInfo", - "cmd:cmd:method:xlMetaV1Object.UnmarshalMsg", - "cmd:cmd:method:xlMetaV2.AddFreeVersion", - "cmd:cmd:method:xlMetaV2.AddLegacy", - "cmd:cmd:method:xlMetaV2.AddVersion", - "cmd:cmd:method:xlMetaV2.AppendTo", - "cmd:cmd:method:xlMetaV2.DeleteVersion", - "cmd:cmd:method:xlMetaV2.ListVersions", - "cmd:cmd:method:xlMetaV2.Load", - "cmd:cmd:method:xlMetaV2.LoadOrConvert", - "cmd:cmd:method:xlMetaV2.SharedDataDirCount", - "cmd:cmd:method:xlMetaV2.SharedDataDirCountStr", - "cmd:cmd:method:xlMetaV2.ToFileInfo", - "cmd:cmd:method:xlMetaV2.UpdateObjectVersion", - "cmd:cmd:method:xlMetaV2DeleteMarker.DecodeMsg", - "cmd:cmd:method:xlMetaV2DeleteMarker.EncodeMsg", - "cmd:cmd:method:xlMetaV2DeleteMarker.FreeVersion", - "cmd:cmd:method:xlMetaV2DeleteMarker.MarshalMsg", - "cmd:cmd:method:xlMetaV2DeleteMarker.Msgsize", - "cmd:cmd:method:xlMetaV2DeleteMarker.Signature", - "cmd:cmd:method:xlMetaV2DeleteMarker.ToFileInfo", - "cmd:cmd:method:xlMetaV2DeleteMarker.UnmarshalMsg", - "cmd:cmd:method:xlMetaV2Object.DecodeMsg", - "cmd:cmd:method:xlMetaV2Object.EncodeMsg", - "cmd:cmd:method:xlMetaV2Object.InitFreeVersion", - "cmd:cmd:method:xlMetaV2Object.InlineData", - "cmd:cmd:method:xlMetaV2Object.MarshalMsg", - "cmd:cmd:method:xlMetaV2Object.Msgsize", - "cmd:cmd:method:xlMetaV2Object.RemoveRestoreHdrs", - "cmd:cmd:method:xlMetaV2Object.ResetInlineData", - "cmd:cmd:method:xlMetaV2Object.SetTransition", - "cmd:cmd:method:xlMetaV2Object.Signature", - "cmd:cmd:method:xlMetaV2Object.ToFileInfo", - "cmd:cmd:method:xlMetaV2Object.UnmarshalMsg", - "cmd:cmd:method:xlMetaV2Object.UsesDataDir", - "cmd:cmd:method:xlMetaV2Version.DecodeMsg", - "cmd:cmd:method:xlMetaV2Version.EncodeMsg", - "cmd:cmd:method:xlMetaV2Version.FreeVersion", - "cmd:cmd:method:xlMetaV2Version.MarshalMsg", - "cmd:cmd:method:xlMetaV2Version.Msgsize", - "cmd:cmd:method:xlMetaV2Version.ToFileInfo", - "cmd:cmd:method:xlMetaV2Version.UnmarshalMsg", - "cmd:cmd:method:xlMetaV2Version.Valid", - "cmd:cmd:method:xlMetaV2VersionHeader.DecodeMsg", - "cmd:cmd:method:xlMetaV2VersionHeader.EncodeMsg", - "cmd:cmd:method:xlMetaV2VersionHeader.FreeVersion", - "cmd:cmd:method:xlMetaV2VersionHeader.InlineData", - "cmd:cmd:method:xlMetaV2VersionHeader.MarshalMsg", - "cmd:cmd:method:xlMetaV2VersionHeader.Msgsize", - "cmd:cmd:method:xlMetaV2VersionHeader.String", - "cmd:cmd:method:xlMetaV2VersionHeader.UnmarshalMsg", - "cmd:cmd:method:xlMetaV2VersionHeader.UsesDataDir", - "cmd:cmd:method:xlMetaV2VersionHeaderV2.DecodeMsg", - "cmd:cmd:method:xlMetaV2VersionHeaderV2.UnmarshalMsg", - "cmd:cmd:method:xlStorage.AppendFile", - "cmd:cmd:method:xlStorage.CheckParts", - "cmd:cmd:method:xlStorage.CleanAbandonedData", - "cmd:cmd:method:xlStorage.Close", - "cmd:cmd:method:xlStorage.CreateFile", - "cmd:cmd:method:xlStorage.Delete", - "cmd:cmd:method:xlStorage.DeleteBulk", - "cmd:cmd:method:xlStorage.DeleteVersion", - "cmd:cmd:method:xlStorage.DeleteVersions", - "cmd:cmd:method:xlStorage.DeleteVol", - "cmd:cmd:method:xlStorage.DiskInfo", - "cmd:cmd:method:xlStorage.Endpoint", - "cmd:cmd:method:xlStorage.GetDiskID", - "cmd:cmd:method:xlStorage.GetDiskLoc", - "cmd:cmd:method:xlStorage.Healing", - "cmd:cmd:method:xlStorage.Hostname", - "cmd:cmd:method:xlStorage.IsLocal", - "cmd:cmd:method:xlStorage.IsOnline", - "cmd:cmd:method:xlStorage.LastConn", - "cmd:cmd:method:xlStorage.ListDir", - "cmd:cmd:method:xlStorage.ListVols", - "cmd:cmd:method:xlStorage.MakeVol", - "cmd:cmd:method:xlStorage.MakeVolBulk", - "cmd:cmd:method:xlStorage.NSScanner", - "cmd:cmd:method:xlStorage.ReadAll", - "cmd:cmd:method:xlStorage.ReadFile", - "cmd:cmd:method:xlStorage.ReadFileStream", - "cmd:cmd:method:xlStorage.ReadParts", - "cmd:cmd:method:xlStorage.ReadVersion", - "cmd:cmd:method:xlStorage.ReadXL", - "cmd:cmd:method:xlStorage.RenameData", - "cmd:cmd:method:xlStorage.RenameFile", - "cmd:cmd:method:xlStorage.RenamePart", - "cmd:cmd:method:xlStorage.SetDiskID", - "cmd:cmd:method:xlStorage.StatInfoFile", - "cmd:cmd:method:xlStorage.StatVol", - "cmd:cmd:method:xlStorage.String", - "cmd:cmd:method:xlStorage.UpdateMetadata", - "cmd:cmd:method:xlStorage.VerifyFile", - "cmd:cmd:method:xlStorage.WalkDir", - "cmd:cmd:method:xlStorage.WriteAll", - "cmd:cmd:method:xlStorage.WriteMetadata", - "cmd:cmd:method:xlStorageDiskIDCheck.AppendFile", - "cmd:cmd:method:xlStorageDiskIDCheck.CheckParts", - "cmd:cmd:method:xlStorageDiskIDCheck.CleanAbandonedData", - "cmd:cmd:method:xlStorageDiskIDCheck.Close", - "cmd:cmd:method:xlStorageDiskIDCheck.CreateFile", - "cmd:cmd:method:xlStorageDiskIDCheck.Delete", - "cmd:cmd:method:xlStorageDiskIDCheck.DeleteBulk", - "cmd:cmd:method:xlStorageDiskIDCheck.DeleteVersion", - "cmd:cmd:method:xlStorageDiskIDCheck.DeleteVersions", - "cmd:cmd:method:xlStorageDiskIDCheck.DeleteVol", - "cmd:cmd:method:xlStorageDiskIDCheck.DiskInfo", - "cmd:cmd:method:xlStorageDiskIDCheck.Endpoint", - "cmd:cmd:method:xlStorageDiskIDCheck.GetDiskID", - "cmd:cmd:method:xlStorageDiskIDCheck.GetDiskLoc", - "cmd:cmd:method:xlStorageDiskIDCheck.Healing", - "cmd:cmd:method:xlStorageDiskIDCheck.Hostname", - "cmd:cmd:method:xlStorageDiskIDCheck.IsLocal", - "cmd:cmd:method:xlStorageDiskIDCheck.IsOnline", - "cmd:cmd:method:xlStorageDiskIDCheck.LastConn", - "cmd:cmd:method:xlStorageDiskIDCheck.ListDir", - "cmd:cmd:method:xlStorageDiskIDCheck.ListVols", - "cmd:cmd:method:xlStorageDiskIDCheck.MakeVol", - "cmd:cmd:method:xlStorageDiskIDCheck.MakeVolBulk", - "cmd:cmd:method:xlStorageDiskIDCheck.NSScanner", - "cmd:cmd:method:xlStorageDiskIDCheck.ReadAll", - "cmd:cmd:method:xlStorageDiskIDCheck.ReadFile", - "cmd:cmd:method:xlStorageDiskIDCheck.ReadFileStream", - "cmd:cmd:method:xlStorageDiskIDCheck.ReadParts", - "cmd:cmd:method:xlStorageDiskIDCheck.ReadVersion", - "cmd:cmd:method:xlStorageDiskIDCheck.ReadXL", - "cmd:cmd:method:xlStorageDiskIDCheck.RenameData", - "cmd:cmd:method:xlStorageDiskIDCheck.RenameFile", - "cmd:cmd:method:xlStorageDiskIDCheck.RenamePart", - "cmd:cmd:method:xlStorageDiskIDCheck.SetDiskID", - "cmd:cmd:method:xlStorageDiskIDCheck.StatInfoFile", - "cmd:cmd:method:xlStorageDiskIDCheck.StatVol", - "cmd:cmd:method:xlStorageDiskIDCheck.String", - "cmd:cmd:method:xlStorageDiskIDCheck.TrackDiskHealth", - "cmd:cmd:method:xlStorageDiskIDCheck.UpdateMetadata", - "cmd:cmd:method:xlStorageDiskIDCheck.VerifyFile", - "cmd:cmd:method:xlStorageDiskIDCheck.WalkDir", - "cmd:cmd:method:xlStorageDiskIDCheck.WriteAll", - "cmd:cmd:method:xlStorageDiskIDCheck.WriteMetadata", - "cmd:cmd:type:APIError", - "cmd:cmd:type:APIErrorCode", - "cmd:cmd:type:APIErrorResponse", - "cmd:cmd:type:AccElem", - "cmd:cmd:type:ActiveWorkerStat", - "cmd:cmd:type:AdminError", - "cmd:cmd:type:AllAccessDisabled", - "cmd:cmd:type:AssumeRoleResponse", - "cmd:cmd:type:AssumeRoleResult", - "cmd:cmd:type:AssumeRoleWithCertificateResponse", - "cmd:cmd:type:AssumeRoleWithClientGrantsResponse", - "cmd:cmd:type:AssumeRoleWithCustomTokenResponse", - "cmd:cmd:type:AssumeRoleWithLDAPResponse", - "cmd:cmd:type:AssumeRoleWithWebIdentityResponse", - "cmd:cmd:type:AssumedRoleUser", - "cmd:cmd:type:AuditLogOptions", - "cmd:cmd:type:BackendDown", - "cmd:cmd:type:BackendType", - "cmd:cmd:type:BaseOptions", - "cmd:cmd:type:BatchJobExpire", - "cmd:cmd:type:BatchJobExpireFilter", - "cmd:cmd:type:BatchJobExpirePurge", - "cmd:cmd:type:BatchJobKV", - "cmd:cmd:type:BatchJobKeyRotateEncryption", - "cmd:cmd:type:BatchJobKeyRotateFlags", - "cmd:cmd:type:BatchJobKeyRotateV1", - "cmd:cmd:type:BatchJobNotification", - "cmd:cmd:type:BatchJobPool", - "cmd:cmd:type:BatchJobPrefix", - "cmd:cmd:type:BatchJobReplicateCredentials", - "cmd:cmd:type:BatchJobReplicateFlags", - "cmd:cmd:type:BatchJobReplicateResourceType", - "cmd:cmd:type:BatchJobReplicateSource", - "cmd:cmd:type:BatchJobReplicateTarget", - "cmd:cmd:type:BatchJobReplicateV1", - "cmd:cmd:type:BatchJobRequest", - "cmd:cmd:type:BatchJobRetry", - "cmd:cmd:type:BatchJobSize", - "cmd:cmd:type:BatchJobSizeFilter", - "cmd:cmd:type:BatchJobSnowball", - "cmd:cmd:type:BatchJobYamlErr", - "cmd:cmd:type:BatchKeyRotateFilter", - "cmd:cmd:type:BatchKeyRotateNotification", - "cmd:cmd:type:BatchKeyRotationType", - "cmd:cmd:type:BatchReplicateFilter", - "cmd:cmd:type:BitrotAlgorithm", - "cmd:cmd:type:BitrotVerifier", - "cmd:cmd:type:Bucket", - "cmd:cmd:type:BucketAccessPolicy", - "cmd:cmd:type:BucketAlreadyExists", - "cmd:cmd:type:BucketAlreadyOwnedByYou", - "cmd:cmd:type:BucketExists", - "cmd:cmd:type:BucketInfo", - "cmd:cmd:type:BucketLifecycleNotFound", - "cmd:cmd:type:BucketMetadata", - "cmd:cmd:type:BucketMetadataSys", - "cmd:cmd:type:BucketMetricsLoaderFn", - "cmd:cmd:type:BucketNameInvalid", - "cmd:cmd:type:BucketNotEmpty", - "cmd:cmd:type:BucketNotFound", - "cmd:cmd:type:BucketObjectLockConfigNotFound", - "cmd:cmd:type:BucketObjectLockSys", - "cmd:cmd:type:BucketOptions", - "cmd:cmd:type:BucketPolicyNotFound", - "cmd:cmd:type:BucketQuotaConfigNotFound", - "cmd:cmd:type:BucketQuotaExceeded", - "cmd:cmd:type:BucketQuotaSys", - "cmd:cmd:type:BucketRemoteAlreadyExists", - "cmd:cmd:type:BucketRemoteArnInvalid", - "cmd:cmd:type:BucketRemoteArnTypeInvalid", - "cmd:cmd:type:BucketRemoteDestinationNotFound", - "cmd:cmd:type:BucketRemoteIdenticalToSource", - "cmd:cmd:type:BucketRemoteLabelInUse", - "cmd:cmd:type:BucketRemoteRemoveDisallowed", - "cmd:cmd:type:BucketRemoteTargetNotFound", - "cmd:cmd:type:BucketRemoteTargetNotVersioned", - "cmd:cmd:type:BucketReplicationConfigNotFound", - "cmd:cmd:type:BucketReplicationResyncStatus", - "cmd:cmd:type:BucketReplicationSourceNotVersioned", - "cmd:cmd:type:BucketReplicationStat", - "cmd:cmd:type:BucketReplicationStats", - "cmd:cmd:type:BucketSSEConfigNotFound", - "cmd:cmd:type:BucketSSEConfigSys", - "cmd:cmd:type:BucketStats", - "cmd:cmd:type:BucketStatsMap", - "cmd:cmd:type:BucketTaggingNotFound", - "cmd:cmd:type:BucketTargetSys", - "cmd:cmd:type:BucketTargetUsageInfo", - "cmd:cmd:type:BucketUsageInfo", - "cmd:cmd:type:BucketVersioningSys", - "cmd:cmd:type:CheckPartsHandlerParams", - "cmd:cmd:type:CheckPartsResp", - "cmd:cmd:type:CheckPreconditionFn", - "cmd:cmd:type:ChecksumAlgo", - "cmd:cmd:type:ChecksumInfo", - "cmd:cmd:type:ClientGrantsResult", - "cmd:cmd:type:CommonPrefix", - "cmd:cmd:type:CompleteMultipartUpload", - "cmd:cmd:type:CompleteMultipartUploadResponse", - "cmd:cmd:type:CompletePart", - "cmd:cmd:type:ConfigDir", - "cmd:cmd:type:ConfigSys", - "cmd:cmd:type:ConsoleLogger", - "cmd:cmd:type:CopyObjectPartResponse", - "cmd:cmd:type:CopyObjectResponse", - "cmd:cmd:type:DailyAllTierStats", - "cmd:cmd:type:DataMovementOverwriteErr", - "cmd:cmd:type:DataUsageInfo", - "cmd:cmd:type:DecryptBlocksReader", - "cmd:cmd:type:DeleteBucketOptions", - "cmd:cmd:type:DeleteBulkReq", - "cmd:cmd:type:DeleteError", - "cmd:cmd:type:DeleteFileHandlerParams", - "cmd:cmd:type:DeleteMarkerMTime", - "cmd:cmd:type:DeleteMarkerVersion", - "cmd:cmd:type:DeleteObjectsRequest", - "cmd:cmd:type:DeleteObjectsResponse", - "cmd:cmd:type:DeleteOptions", - "cmd:cmd:type:DeleteVersionHandlerParams", - "cmd:cmd:type:DeleteVersionsErrsResp", - "cmd:cmd:type:DeletedObject", - "cmd:cmd:type:DeletedObjectInfo", - "cmd:cmd:type:DeletedObjectReplicationInfo", - "cmd:cmd:type:DiskInfo", - "cmd:cmd:type:DiskInfoOptions", - "cmd:cmd:type:DiskMetrics", - "cmd:cmd:type:Encryption", - "cmd:cmd:type:Endpoint", - "cmd:cmd:type:EndpointServerPools", - "cmd:cmd:type:EndpointType", - "cmd:cmd:type:Endpoints", - "cmd:cmd:type:Erasure", - "cmd:cmd:type:ErasureAlgo", - "cmd:cmd:type:ErasureInfo", - "cmd:cmd:type:EvalMetadataFn", - "cmd:cmd:type:EvalRetentionBypassFn", - "cmd:cmd:type:EventNotifier", - "cmd:cmd:type:ExpirationOptions", - "cmd:cmd:type:FileInfo", - "cmd:cmd:type:FileInfoVersions", - "cmd:cmd:type:FileLogger", - "cmd:cmd:type:FilesInfo", - "cmd:cmd:type:GenericError", - "cmd:cmd:type:GetObjectInfoFn", - "cmd:cmd:type:GetObjectReader", - "cmd:cmd:type:GroupInfo", - "cmd:cmd:type:HTTPAPIStats", - "cmd:cmd:type:HTTPConsoleLoggerSys", - "cmd:cmd:type:HTTPRangeSpec", - "cmd:cmd:type:HTTPStats", - "cmd:cmd:type:HealObjectFn", - "cmd:cmd:type:HealthOptions", - "cmd:cmd:type:HealthResult", - "cmd:cmd:type:Help", - "cmd:cmd:type:IAMEtcdStore", - "cmd:cmd:type:IAMObjectStore", - "cmd:cmd:type:IAMStorageAPI", - "cmd:cmd:type:IAMStoreSys", - "cmd:cmd:type:IAMSys", - "cmd:cmd:type:IAMUserType", - "cmd:cmd:type:InQueueMetric", - "cmd:cmd:type:InQueueStats", - "cmd:cmd:type:IncompleteBody", - "cmd:cmd:type:InitiateMultipartUploadResponse", - "cmd:cmd:type:Initiator", - "cmd:cmd:type:InsufficientReadQuorum", - "cmd:cmd:type:InsufficientWriteQuorum", - "cmd:cmd:type:InvalidArgument", - "cmd:cmd:type:InvalidETag", - "cmd:cmd:type:InvalidObjectState", - "cmd:cmd:type:InvalidPart", - "cmd:cmd:type:InvalidRange", - "cmd:cmd:type:InvalidUploadID", - "cmd:cmd:type:InvalidUploadIDKeyCombination", - "cmd:cmd:type:InvalidVersionID", - "cmd:cmd:type:KMSLogger", - "cmd:cmd:type:LDAPIdentityResult", - "cmd:cmd:type:LastMinuteHistogram", - "cmd:cmd:type:LifecycleSys", - "cmd:cmd:type:ListBucketsResponse", - "cmd:cmd:type:ListDirResult", - "cmd:cmd:type:ListMultipartUploadsResponse", - "cmd:cmd:type:ListMultipartsInfo", - "cmd:cmd:type:ListObjectVersionsInfo", - "cmd:cmd:type:ListObjectsInfo", - "cmd:cmd:type:ListObjectsResponse", - "cmd:cmd:type:ListObjectsV2Info", - "cmd:cmd:type:ListObjectsV2Response", - "cmd:cmd:type:ListPartsInfo", - "cmd:cmd:type:ListPartsResponse", - "cmd:cmd:type:ListVersionsResponse", - "cmd:cmd:type:LocalDiskIDs", - "cmd:cmd:type:LocationResponse", - "cmd:cmd:type:LockContext", - "cmd:cmd:type:MRFReplicateEntries", - "cmd:cmd:type:MRFReplicateEntry", - "cmd:cmd:type:MakeBucketOptions", - "cmd:cmd:type:MalformedUploadID", - "cmd:cmd:type:MappedPolicy", - "cmd:cmd:type:Metadata", - "cmd:cmd:type:MetadataEntry", - "cmd:cmd:type:MetadataHandlerParams", - "cmd:cmd:type:MethodNotAllowed", - "cmd:cmd:type:MetricDescription", - "cmd:cmd:type:MetricDescriptor", - "cmd:cmd:type:MetricName", - "cmd:cmd:type:MetricNamespace", - "cmd:cmd:type:MetricSubsystem", - "cmd:cmd:type:MetricType", - "cmd:cmd:type:MetricTypeV2", - "cmd:cmd:type:MetricV2", - "cmd:cmd:type:MetricValues", - "cmd:cmd:type:MetricsGroup", - "cmd:cmd:type:MetricsGroupOpts", - "cmd:cmd:type:MetricsGroupV2", - "cmd:cmd:type:MetricsLoaderFn", - "cmd:cmd:type:MultipartInfo", - "cmd:cmd:type:NewMultipartUploadResult", - "cmd:cmd:type:Node", - "cmd:cmd:type:NotImplemented", - "cmd:cmd:type:NotificationGroup", - "cmd:cmd:type:NotificationPeerErr", - "cmd:cmd:type:NotificationSys", - "cmd:cmd:type:ObjReaderFn", - "cmd:cmd:type:Object", - "cmd:cmd:type:ObjectAlreadyExists", - "cmd:cmd:type:ObjectExistsAsDirectory", - "cmd:cmd:type:ObjectInfo", - "cmd:cmd:type:ObjectInternalInfo", - "cmd:cmd:type:ObjectLayer", - "cmd:cmd:type:ObjectLocked", - "cmd:cmd:type:ObjectNameInvalid", - "cmd:cmd:type:ObjectNamePrefixAsSlash", - "cmd:cmd:type:ObjectNameTooLong", - "cmd:cmd:type:ObjectNotFound", - "cmd:cmd:type:ObjectOptions", - "cmd:cmd:type:ObjectPartInfo", - "cmd:cmd:type:ObjectTagSet", - "cmd:cmd:type:ObjectToDelete", - "cmd:cmd:type:ObjectTooLarge", - "cmd:cmd:type:ObjectTooSmall", - "cmd:cmd:type:ObjectV", - "cmd:cmd:type:ObjectVersion", - "cmd:cmd:type:OpenIDClientAppParams", - "cmd:cmd:type:OperationTimedOut", - "cmd:cmd:type:OutputLocation", - "cmd:cmd:type:Owner", - "cmd:cmd:type:ParentUserInfo", - "cmd:cmd:type:Part", - "cmd:cmd:type:PartInfo", - "cmd:cmd:type:PartTooBig", - "cmd:cmd:type:PartTooSmall", - "cmd:cmd:type:PartialOperation", - "cmd:cmd:type:PeerLocks", - "cmd:cmd:type:PeerResourceMetrics", - "cmd:cmd:type:PeerSiteInfo", - "cmd:cmd:type:PolicyDoc", - "cmd:cmd:type:PolicyStatus", - "cmd:cmd:type:PolicySys", - "cmd:cmd:type:PoolDecommissionInfo", - "cmd:cmd:type:PoolEndpointList", - "cmd:cmd:type:PoolEndpoints", - "cmd:cmd:type:PoolObjInfo", - "cmd:cmd:type:PoolStatus", - "cmd:cmd:type:PostPolicyForm", - "cmd:cmd:type:PostResponse", - "cmd:cmd:type:PreConditionFailed", - "cmd:cmd:type:PrefixAccessDenied", - "cmd:cmd:type:ProxyEndpoint", - "cmd:cmd:type:ProxyMetric", - "cmd:cmd:type:PutObjReader", - "cmd:cmd:type:QStat", - "cmd:cmd:type:RMetricName", - "cmd:cmd:type:RQErrType", - "cmd:cmd:type:RStat", - "cmd:cmd:type:RTimedMetrics", - "cmd:cmd:type:RWLocker", - "cmd:cmd:type:RawFileInfo", - "cmd:cmd:type:ReadAllHandlerParams", - "cmd:cmd:type:ReadOptions", - "cmd:cmd:type:ReadPartsReq", - "cmd:cmd:type:ReadPartsResp", - "cmd:cmd:type:RemoteTargetConnectionErr", - "cmd:cmd:type:RenameDataHandlerParams", - "cmd:cmd:type:RenameDataInlineHandlerParams", - "cmd:cmd:type:RenameDataResp", - "cmd:cmd:type:RenameFileHandlerParams", - "cmd:cmd:type:RenameOptions", - "cmd:cmd:type:RenamePartHandlerParams", - "cmd:cmd:type:ReplQNodeStats", - "cmd:cmd:type:ReplicateDecision", - "cmd:cmd:type:ReplicateObjectInfo", - "cmd:cmd:type:ReplicationLastHour", - "cmd:cmd:type:ReplicationLastMinute", - "cmd:cmd:type:ReplicationLatency", - "cmd:cmd:type:ReplicationMRFStats", - "cmd:cmd:type:ReplicationPermissionCheck", - "cmd:cmd:type:ReplicationPool", - "cmd:cmd:type:ReplicationQueueStats", - "cmd:cmd:type:ReplicationState", - "cmd:cmd:type:ReplicationStats", - "cmd:cmd:type:ReplicationWorkerOperation", - "cmd:cmd:type:ResourceMetric", - "cmd:cmd:type:ResourceMetrics", - "cmd:cmd:type:RestoreObjectRequest", - "cmd:cmd:type:RestoreRequestType", - "cmd:cmd:type:ResyncDecision", - "cmd:cmd:type:ResyncStatusType", - "cmd:cmd:type:ResyncTarget", - "cmd:cmd:type:ResyncTargetDecision", - "cmd:cmd:type:ResyncTargetsInfo", - "cmd:cmd:type:S3Location", - "cmd:cmd:type:S3PeerSys", - "cmd:cmd:type:SMA", - "cmd:cmd:type:SRBucketDeleteOp", - "cmd:cmd:type:SRError", - "cmd:cmd:type:SRMetric", - "cmd:cmd:type:SRMetricsSummary", - "cmd:cmd:type:SRStats", - "cmd:cmd:type:SRStatus", - "cmd:cmd:type:STSError", - "cmd:cmd:type:STSErrorCode", - "cmd:cmd:type:STSErrorResponse", - "cmd:cmd:type:SealMD5CurrFn", - "cmd:cmd:type:SelectParameters", - "cmd:cmd:type:ServerHTTPAPIStats", - "cmd:cmd:type:ServerHTTPStats", - "cmd:cmd:type:ServerProperties", - "cmd:cmd:type:ServerSystemConfig", - "cmd:cmd:type:SetupType", - "cmd:cmd:type:SignatureDoesNotMatch", - "cmd:cmd:type:SiteReplicationSys", - "cmd:cmd:type:SiteResyncStatus", - "cmd:cmd:type:SlowDown", - "cmd:cmd:type:SpeedTestResult", - "cmd:cmd:type:StartProfilingResult", - "cmd:cmd:type:StatInfo", - "cmd:cmd:type:StorageAPI", - "cmd:cmd:type:StorageErr", - "cmd:cmd:type:StorageFull", - "cmd:cmd:type:StorageInfo", - "cmd:cmd:type:TargetClient", - "cmd:cmd:type:TargetReplicationResyncStatus", - "cmd:cmd:type:TierConfigMgr", - "cmd:cmd:type:TransitionOptions", - "cmd:cmd:type:TransitionStorageClassNotFound", - "cmd:cmd:type:TransitionedObject", - "cmd:cmd:type:UnsupportedMetadata", - "cmd:cmd:type:UpdateMetadataOpts", - "cmd:cmd:type:Upload", - "cmd:cmd:type:UserIdentity", - "cmd:cmd:type:UsersSysType", - "cmd:cmd:type:VersionNotFound", - "cmd:cmd:type:VersionPurgeStatusType", - "cmd:cmd:type:VersionType", - "cmd:cmd:type:VolInfo", - "cmd:cmd:type:VolsInfo", - "cmd:cmd:type:WalkDirOptions", - "cmd:cmd:type:WalkOptions", - "cmd:cmd:type:WalkVersionsSortOrder", - "cmd:cmd:type:WarmBackend", - "cmd:cmd:type:WarmBackendGetOpts", - "cmd:cmd:type:WebIdentityResult", - "cmd:cmd:type:WriteAllHandlerParams", - "cmd:cmd:type:XferStats", - "cmd:cmd:var:CommitID", - "cmd:cmd:var:CopyrightYear", - "cmd:cmd:var:GOPATH", - "cmd:cmd:var:GOROOT", - "cmd:cmd:var:GlobalContext", - "cmd:cmd:var:GlobalFlags", - "cmd:cmd:var:GlobalKMS", - "cmd:cmd:var:MinioBannerName", - "cmd:cmd:var:MinioLicense", - "cmd:cmd:var:MinioReleaseBaseURL", - "cmd:cmd:var:MinioReleaseTagTimeLayout", - "cmd:cmd:var:MinioReleaseURL", - "cmd:cmd:var:MinioStoreName", - "cmd:cmd:var:MinioUAName", - "cmd:cmd:var:ObjectsHistogramIntervals", - "cmd:cmd:var:ObjectsHistogramIntervalsV1", - "cmd:cmd:var:ObjectsVersionCountIntervals", - "cmd:cmd:var:OfflineDisk", - "cmd:cmd:var:ReleaseTag", - "cmd:cmd:var:ServerFlags", - "cmd:cmd:var:ShortCommitID", - "cmd:cmd:var:Version", - "docs/debugging/inspect:main:method:xlMetaV2VersionHeaderV2.MarshalJSON", - "docs/debugging/inspect:main:method:xlMetaV2VersionHeaderV2.UnmarshalMsg", - "docs/debugging/xl-meta:main:method:xlMetaV2VersionHeaderV2.MarshalJSON", - "docs/debugging/xl-meta:main:method:xlMetaV2VersionHeaderV2.UnmarshalMsg", - "docs/iam:main:field:Resp.Claims", - "docs/iam:main:field:Resp.MaxValiditySeconds", - "docs/iam:main:field:Resp.User", - "docs/iam:main:field:Result.Result", - "docs/iam:main:type:Resp", - "docs/iam:main:type:Result", - "docs/sts:main:field:DiscoveryDoc.AuthEndpoint", - "docs/sts:main:field:DiscoveryDoc.ClaimsSupported", - "docs/sts:main:field:DiscoveryDoc.CodeChallengeMethodsSupported", - "docs/sts:main:field:DiscoveryDoc.IDTokenSigningAlgValuesSupported", - "docs/sts:main:field:DiscoveryDoc.Issuer", - "docs/sts:main:field:DiscoveryDoc.JwksURI", - "docs/sts:main:field:DiscoveryDoc.ResponseTypesSupported", - "docs/sts:main:field:DiscoveryDoc.RevocationEndpoint", - "docs/sts:main:field:DiscoveryDoc.ScopesSupported", - "docs/sts:main:field:DiscoveryDoc.SubjectTypesSupported", - "docs/sts:main:field:DiscoveryDoc.TokenEndpoint", - "docs/sts:main:field:DiscoveryDoc.TokenEndpointAuthMethods", - "docs/sts:main:field:DiscoveryDoc.UserInfoEndpoint", - "docs/sts:main:field:JWTToken.AccessToken", - "docs/sts:main:field:JWTToken.Expiry", - "docs/sts:main:type:DiscoveryDoc", - "docs/sts:main:type:JWTToken", - "internal/amztime:amztime:func:ISO8601Format", - "internal/amztime:amztime:func:ISO8601Parse", - "internal/amztime:amztime:func:Parse", - "internal/amztime:amztime:func:ParseHeader", - "internal/amztime:amztime:func:ParseReplicationTS", - "internal/amztime:amztime:var:ErrMalformedDate", - "internal/arn:arn:field:ARN.Partition", - "internal/arn:arn:field:ARN.Region", - "internal/arn:arn:field:ARN.ResourceID", - "internal/arn:arn:field:ARN.ResourceType", - "internal/arn:arn:field:ARN.Service", - "internal/arn:arn:func:NewIAMRoleARN", - "internal/arn:arn:func:Parse", - "internal/arn:arn:method:ARN.String", - "internal/arn:arn:type:ARN", - "internal/auth:auth:const:AccountOff", - "internal/auth:auth:const:AccountOn", - "internal/auth:auth:const:DefaultAccessKey", - "internal/auth:auth:const:DefaultSecretKey", - "internal/auth:auth:field:Credentials.AccessKey", - "internal/auth:auth:field:Credentials.Claims", - "internal/auth:auth:field:Credentials.Comment", - "internal/auth:auth:field:Credentials.Description", - "internal/auth:auth:field:Credentials.Expiration", - "internal/auth:auth:field:Credentials.Groups", - "internal/auth:auth:field:Credentials.Name", - "internal/auth:auth:field:Credentials.ParentUser", - "internal/auth:auth:field:Credentials.SecretKey", - "internal/auth:auth:field:Credentials.SessionToken", - "internal/auth:auth:field:Credentials.Status", - "internal/auth:auth:func:ContainsReservedChars", - "internal/auth:auth:func:CreateCredentials", - "internal/auth:auth:func:CreateNewCredentialsWithMetadata", - "internal/auth:auth:func:ExpToInt64", - "internal/auth:auth:func:ExtractClaims", - "internal/auth:auth:func:GenerateAccessKey", - "internal/auth:auth:func:GenerateCredentials", - "internal/auth:auth:func:GenerateSecretKey", - "internal/auth:auth:func:GetNewCredentials", - "internal/auth:auth:func:GetNewCredentialsWithMetadata", - "internal/auth:auth:func:IsAccessKeyValid", - "internal/auth:auth:func:IsSecretKeyValid", - "internal/auth:auth:func:JWTSignWithAccessKey", - "internal/auth:auth:method:Credentials.Equal", - "internal/auth:auth:method:Credentials.IsExpired", - "internal/auth:auth:method:Credentials.IsImpliedPolicy", - "internal/auth:auth:method:Credentials.IsServiceAccount", - "internal/auth:auth:method:Credentials.IsTemp", - "internal/auth:auth:method:Credentials.IsValid", - "internal/auth:auth:method:Credentials.String", - "internal/auth:auth:type:Credentials", - "internal/auth:auth:var:AnonymousCredentials", - "internal/auth:auth:var:DefaultCredentials", - "internal/auth:auth:var:ErrContainsReservedChars", - "internal/auth:auth:var:ErrInvalidAccessKeyLength", - "internal/auth:auth:var:ErrInvalidDuration", - "internal/auth:auth:var:ErrInvalidSecretKeyLength", - "internal/auth:auth:var:ErrNoAccessKeyWithSecretKey", - "internal/auth:auth:var:ErrNoSecretKeyWithAccessKey", - "internal/bpool:bpool:field:Pool.New", - "internal/bpool:bpool:func:NewBytePoolCap", - "internal/bpool:bpool:method:BytePoolCap.CurrentSize", - "internal/bpool:bpool:method:BytePoolCap.Get", - "internal/bpool:bpool:method:BytePoolCap.Populate", - "internal/bpool:bpool:method:BytePoolCap.Put", - "internal/bpool:bpool:method:BytePoolCap.Width", - "internal/bpool:bpool:method:BytePoolCap.WidthCap", - "internal/bpool:bpool:method:Pool.Get", - "internal/bpool:bpool:method:Pool.Put", - "internal/bpool:bpool:type:BytePoolCap", - "internal/bpool:bpool:type:Pool", - "internal/bucket/bandwidth:bandwidth:field:BucketBandwidthReport.BucketStats", - "internal/bucket/bandwidth:bandwidth:field:BucketOptions.Name", - "internal/bucket/bandwidth:bandwidth:field:BucketOptions.ReplicationARN", - "internal/bucket/bandwidth:bandwidth:field:Details.CurrentBandwidthInBytesPerSecond", - "internal/bucket/bandwidth:bandwidth:field:Details.LimitInBytesPerSecond", - "internal/bucket/bandwidth:bandwidth:field:Monitor.NodeCount", - "internal/bucket/bandwidth:bandwidth:field:MonitorReaderOptions.HeaderSize", - "internal/bucket/bandwidth:bandwidth:func:NewMonitor", - "internal/bucket/bandwidth:bandwidth:func:NewMonitoredReader", - "internal/bucket/bandwidth:bandwidth:func:SelectBuckets", - "internal/bucket/bandwidth:bandwidth:method:BucketBandwidthReport.DecodeMsg", - "internal/bucket/bandwidth:bandwidth:method:BucketBandwidthReport.EncodeMsg", - "internal/bucket/bandwidth:bandwidth:method:BucketBandwidthReport.MarshalMsg", - "internal/bucket/bandwidth:bandwidth:method:BucketBandwidthReport.Msgsize", - "internal/bucket/bandwidth:bandwidth:method:BucketBandwidthReport.UnmarshalMsg", - "internal/bucket/bandwidth:bandwidth:method:Details.DecodeMsg", - "internal/bucket/bandwidth:bandwidth:method:Details.EncodeMsg", - "internal/bucket/bandwidth:bandwidth:method:Details.MarshalMsg", - "internal/bucket/bandwidth:bandwidth:method:Details.Msgsize", - "internal/bucket/bandwidth:bandwidth:method:Details.UnmarshalMsg", - "internal/bucket/bandwidth:bandwidth:method:Monitor.DeleteBucket", - "internal/bucket/bandwidth:bandwidth:method:Monitor.DeleteBucketThrottle", - "internal/bucket/bandwidth:bandwidth:method:Monitor.GetReport", - "internal/bucket/bandwidth:bandwidth:method:Monitor.IsThrottled", - "internal/bucket/bandwidth:bandwidth:method:Monitor.SetBandwidthLimit", - "internal/bucket/bandwidth:bandwidth:method:MonitoredReader.Read", - "internal/bucket/bandwidth:bandwidth:type:BucketBandwidthReport", - "internal/bucket/bandwidth:bandwidth:type:BucketOptions", - "internal/bucket/bandwidth:bandwidth:type:Details", - "internal/bucket/bandwidth:bandwidth:type:Monitor", - "internal/bucket/bandwidth:bandwidth:type:MonitorReaderOptions", - "internal/bucket/bandwidth:bandwidth:type:MonitoredReader", - "internal/bucket/bandwidth:bandwidth:type:SelectionFunction", - "internal/bucket/encryption:sse:const:AES256", - "internal/bucket/encryption:sse:const:AWSKms", - "internal/bucket/encryption:sse:field:ApplyOptions.AutoEncrypt", - "internal/bucket/encryption:sse:field:BucketSSEConfig.Rules", - "internal/bucket/encryption:sse:field:BucketSSEConfig.XMLNS", - "internal/bucket/encryption:sse:field:BucketSSEConfig.XMLName", - "internal/bucket/encryption:sse:field:EncryptionAction.Algorithm", - "internal/bucket/encryption:sse:field:EncryptionAction.MasterKeyID", - "internal/bucket/encryption:sse:field:Rule.DefaultEncryptionAction", - "internal/bucket/encryption:sse:func:ParseBucketSSEConfig", - "internal/bucket/encryption:sse:method:Algorithm.MarshalXML", - "internal/bucket/encryption:sse:method:Algorithm.UnmarshalXML", - "internal/bucket/encryption:sse:method:BucketSSEConfig.Algo", - "internal/bucket/encryption:sse:method:BucketSSEConfig.Apply", - "internal/bucket/encryption:sse:method:BucketSSEConfig.KeyID", - "internal/bucket/encryption:sse:type:Algorithm", - "internal/bucket/encryption:sse:type:ApplyOptions", - "internal/bucket/encryption:sse:type:BucketSSEConfig", - "internal/bucket/encryption:sse:type:EncryptionAction", - "internal/bucket/encryption:sse:type:Rule", - "internal/bucket/lifecycle:lifecycle:const:ActionCount", - "internal/bucket/lifecycle:lifecycle:const:DelMarkerDeleteAllVersionsAction", - "internal/bucket/lifecycle:lifecycle:const:DeleteAction", - "internal/bucket/lifecycle:lifecycle:const:DeleteAllVersionsAction", - "internal/bucket/lifecycle:lifecycle:const:DeleteRestoredAction", - "internal/bucket/lifecycle:lifecycle:const:DeleteRestoredVersionAction", - "internal/bucket/lifecycle:lifecycle:const:DeleteVersionAction", - "internal/bucket/lifecycle:lifecycle:const:Disabled", - "internal/bucket/lifecycle:lifecycle:const:Enabled", - "internal/bucket/lifecycle:lifecycle:const:NoneAction", - "internal/bucket/lifecycle:lifecycle:const:TransitionAction", - "internal/bucket/lifecycle:lifecycle:const:TransitionComplete", - "internal/bucket/lifecycle:lifecycle:const:TransitionPending", - "internal/bucket/lifecycle:lifecycle:const:TransitionVersionAction", - "internal/bucket/lifecycle:lifecycle:field:And.ObjectSizeGreaterThan", - "internal/bucket/lifecycle:lifecycle:field:And.ObjectSizeLessThan", - "internal/bucket/lifecycle:lifecycle:field:And.Prefix", - "internal/bucket/lifecycle:lifecycle:field:And.Tags", - "internal/bucket/lifecycle:lifecycle:field:And.XMLName", - "internal/bucket/lifecycle:lifecycle:field:Boolean.Unused", - "internal/bucket/lifecycle:lifecycle:field:DelMarkerExpiration.Days", - "internal/bucket/lifecycle:lifecycle:field:DelMarkerExpiration.XMLName", - "internal/bucket/lifecycle:lifecycle:field:Event.Action", - "internal/bucket/lifecycle:lifecycle:field:Event.Due", - "internal/bucket/lifecycle:lifecycle:field:Event.NewerNoncurrentVersions", - "internal/bucket/lifecycle:lifecycle:field:Event.NoncurrentDays", - "internal/bucket/lifecycle:lifecycle:field:Event.RuleID", - "internal/bucket/lifecycle:lifecycle:field:Event.StorageClass", - "internal/bucket/lifecycle:lifecycle:field:Expiration.Date", - "internal/bucket/lifecycle:lifecycle:field:Expiration.Days", - "internal/bucket/lifecycle:lifecycle:field:Expiration.DeleteAll", - "internal/bucket/lifecycle:lifecycle:field:Expiration.DeleteMarker", - "internal/bucket/lifecycle:lifecycle:field:Expiration.XMLName", - "internal/bucket/lifecycle:lifecycle:field:Filter.And", - "internal/bucket/lifecycle:lifecycle:field:Filter.ObjectSizeGreaterThan", - "internal/bucket/lifecycle:lifecycle:field:Filter.ObjectSizeLessThan", - "internal/bucket/lifecycle:lifecycle:field:Filter.Prefix", - "internal/bucket/lifecycle:lifecycle:field:Filter.Tag", - "internal/bucket/lifecycle:lifecycle:field:Filter.XMLName", - "internal/bucket/lifecycle:lifecycle:field:Lifecycle.ExpiryUpdatedAt", - "internal/bucket/lifecycle:lifecycle:field:Lifecycle.Rules", - "internal/bucket/lifecycle:lifecycle:field:Lifecycle.XMLName", - "internal/bucket/lifecycle:lifecycle:field:NoncurrentVersionExpiration.NewerNoncurrentVersions", - "internal/bucket/lifecycle:lifecycle:field:NoncurrentVersionExpiration.NoncurrentDays", - "internal/bucket/lifecycle:lifecycle:field:NoncurrentVersionExpiration.XMLName", - "internal/bucket/lifecycle:lifecycle:field:NoncurrentVersionTransition.NoncurrentDays", - "internal/bucket/lifecycle:lifecycle:field:NoncurrentVersionTransition.StorageClass", - "internal/bucket/lifecycle:lifecycle:field:ObjectOpts.DeleteMarker", - "internal/bucket/lifecycle:lifecycle:field:ObjectOpts.IsLatest", - "internal/bucket/lifecycle:lifecycle:field:ObjectOpts.ModTime", - "internal/bucket/lifecycle:lifecycle:field:ObjectOpts.Name", - "internal/bucket/lifecycle:lifecycle:field:ObjectOpts.NumVersions", - "internal/bucket/lifecycle:lifecycle:field:ObjectOpts.ReplicationStatus", - "internal/bucket/lifecycle:lifecycle:field:ObjectOpts.RestoreExpires", - "internal/bucket/lifecycle:lifecycle:field:ObjectOpts.RestoreOngoing", - "internal/bucket/lifecycle:lifecycle:field:ObjectOpts.Size", - "internal/bucket/lifecycle:lifecycle:field:ObjectOpts.SuccessorModTime", - "internal/bucket/lifecycle:lifecycle:field:ObjectOpts.TransitionStatus", - "internal/bucket/lifecycle:lifecycle:field:ObjectOpts.UserDefined", - "internal/bucket/lifecycle:lifecycle:field:ObjectOpts.UserTags", - "internal/bucket/lifecycle:lifecycle:field:ObjectOpts.VersionID", - "internal/bucket/lifecycle:lifecycle:field:ObjectOpts.VersionPurgeStatus", - "internal/bucket/lifecycle:lifecycle:field:Prefix.Unused", - "internal/bucket/lifecycle:lifecycle:field:Rule.DelMarkerExpiration", - "internal/bucket/lifecycle:lifecycle:field:Rule.Expiration", - "internal/bucket/lifecycle:lifecycle:field:Rule.Filter", - "internal/bucket/lifecycle:lifecycle:field:Rule.ID", - "internal/bucket/lifecycle:lifecycle:field:Rule.NoncurrentVersionExpiration", - "internal/bucket/lifecycle:lifecycle:field:Rule.NoncurrentVersionTransition", - "internal/bucket/lifecycle:lifecycle:field:Rule.Prefix", - "internal/bucket/lifecycle:lifecycle:field:Rule.Status", - "internal/bucket/lifecycle:lifecycle:field:Rule.Transition", - "internal/bucket/lifecycle:lifecycle:field:Rule.XMLName", - "internal/bucket/lifecycle:lifecycle:field:Tag.Key", - "internal/bucket/lifecycle:lifecycle:field:Tag.Value", - "internal/bucket/lifecycle:lifecycle:field:Tag.XMLName", - "internal/bucket/lifecycle:lifecycle:field:Transition.Date", - "internal/bucket/lifecycle:lifecycle:field:Transition.Days", - "internal/bucket/lifecycle:lifecycle:field:Transition.StorageClass", - "internal/bucket/lifecycle:lifecycle:field:Transition.XMLName", - "internal/bucket/lifecycle:lifecycle:func:Errorf", - "internal/bucket/lifecycle:lifecycle:func:ExpectedExpiryTime", - "internal/bucket/lifecycle:lifecycle:func:NewEvaluator", - "internal/bucket/lifecycle:lifecycle:func:ParseLifecycleConfig", - "internal/bucket/lifecycle:lifecycle:func:ParseLifecycleConfigWithID", - "internal/bucket/lifecycle:lifecycle:method:Action.Delete", - "internal/bucket/lifecycle:lifecycle:method:Action.DeleteAll", - "internal/bucket/lifecycle:lifecycle:method:Action.DeleteRestored", - "internal/bucket/lifecycle:lifecycle:method:Action.DeleteVersioned", - "internal/bucket/lifecycle:lifecycle:method:Action.String", - "internal/bucket/lifecycle:lifecycle:method:And.BySize", - "internal/bucket/lifecycle:lifecycle:method:And.ContainsDuplicateTag", - "internal/bucket/lifecycle:lifecycle:method:And.Validate", - "internal/bucket/lifecycle:lifecycle:method:Boolean.MarshalXML", - "internal/bucket/lifecycle:lifecycle:method:Boolean.UnmarshalXML", - "internal/bucket/lifecycle:lifecycle:method:DelMarkerExpiration.Empty", - "internal/bucket/lifecycle:lifecycle:method:DelMarkerExpiration.MarshalXML", - "internal/bucket/lifecycle:lifecycle:method:DelMarkerExpiration.NextDue", - "internal/bucket/lifecycle:lifecycle:method:DelMarkerExpiration.UnmarshalXML", - "internal/bucket/lifecycle:lifecycle:method:Error.Error", - "internal/bucket/lifecycle:lifecycle:method:Error.Unwrap", - "internal/bucket/lifecycle:lifecycle:method:Evaluator.Eval", - "internal/bucket/lifecycle:lifecycle:method:Evaluator.IsObjectLocked", - "internal/bucket/lifecycle:lifecycle:method:Evaluator.IsPendingReplication", - "internal/bucket/lifecycle:lifecycle:method:Evaluator.WithLockRetention", - "internal/bucket/lifecycle:lifecycle:method:Evaluator.WithReplicationConfig", - "internal/bucket/lifecycle:lifecycle:method:Expiration.IsDateNull", - "internal/bucket/lifecycle:lifecycle:method:Expiration.IsDaysNull", - "internal/bucket/lifecycle:lifecycle:method:Expiration.IsNull", - "internal/bucket/lifecycle:lifecycle:method:Expiration.MarshalXML", - "internal/bucket/lifecycle:lifecycle:method:Expiration.UnmarshalXML", - "internal/bucket/lifecycle:lifecycle:method:Expiration.Validate", - "internal/bucket/lifecycle:lifecycle:method:ExpirationDate.MarshalXML", - "internal/bucket/lifecycle:lifecycle:method:ExpirationDate.UnmarshalXML", - "internal/bucket/lifecycle:lifecycle:method:ExpirationDays.MarshalXML", - "internal/bucket/lifecycle:lifecycle:method:ExpirationDays.UnmarshalXML", - "internal/bucket/lifecycle:lifecycle:method:Filter.BySize", - "internal/bucket/lifecycle:lifecycle:method:Filter.IsEmpty", - "internal/bucket/lifecycle:lifecycle:method:Filter.MarshalXML", - "internal/bucket/lifecycle:lifecycle:method:Filter.TestTags", - "internal/bucket/lifecycle:lifecycle:method:Filter.UnmarshalXML", - "internal/bucket/lifecycle:lifecycle:method:Filter.Validate", - "internal/bucket/lifecycle:lifecycle:method:Lifecycle.Eval", - "internal/bucket/lifecycle:lifecycle:method:Lifecycle.FilterRules", - "internal/bucket/lifecycle:lifecycle:method:Lifecycle.HasActiveRules", - "internal/bucket/lifecycle:lifecycle:method:Lifecycle.HasExpiry", - "internal/bucket/lifecycle:lifecycle:method:Lifecycle.HasTransition", - "internal/bucket/lifecycle:lifecycle:method:Lifecycle.NoncurrentVersionsExpirationLimit", - "internal/bucket/lifecycle:lifecycle:method:Lifecycle.SetPredictionHeaders", - "internal/bucket/lifecycle:lifecycle:method:Lifecycle.UnmarshalXML", - "internal/bucket/lifecycle:lifecycle:method:Lifecycle.Validate", - "internal/bucket/lifecycle:lifecycle:method:NoncurrentVersionExpiration.IsDaysNull", - "internal/bucket/lifecycle:lifecycle:method:NoncurrentVersionExpiration.IsNull", - "internal/bucket/lifecycle:lifecycle:method:NoncurrentVersionExpiration.MarshalXML", - "internal/bucket/lifecycle:lifecycle:method:NoncurrentVersionExpiration.UnmarshalXML", - "internal/bucket/lifecycle:lifecycle:method:NoncurrentVersionExpiration.Validate", - "internal/bucket/lifecycle:lifecycle:method:NoncurrentVersionTransition.IsNull", - "internal/bucket/lifecycle:lifecycle:method:NoncurrentVersionTransition.MarshalXML", - "internal/bucket/lifecycle:lifecycle:method:NoncurrentVersionTransition.NextDue", - "internal/bucket/lifecycle:lifecycle:method:NoncurrentVersionTransition.UnmarshalXML", - "internal/bucket/lifecycle:lifecycle:method:NoncurrentVersionTransition.Validate", - "internal/bucket/lifecycle:lifecycle:method:ObjectOpts.ExpiredObjectDeleteMarker", - "internal/bucket/lifecycle:lifecycle:method:Prefix.MarshalXML", - "internal/bucket/lifecycle:lifecycle:method:Prefix.String", - "internal/bucket/lifecycle:lifecycle:method:Prefix.UnmarshalXML", - "internal/bucket/lifecycle:lifecycle:method:Rule.CloneNonTransition", - "internal/bucket/lifecycle:lifecycle:method:Rule.GetPrefix", - "internal/bucket/lifecycle:lifecycle:method:Rule.Tags", - "internal/bucket/lifecycle:lifecycle:method:Rule.Validate", - "internal/bucket/lifecycle:lifecycle:method:Tag.IsEmpty", - "internal/bucket/lifecycle:lifecycle:method:Tag.String", - "internal/bucket/lifecycle:lifecycle:method:Tag.UnmarshalXML", - "internal/bucket/lifecycle:lifecycle:method:Tag.Validate", - "internal/bucket/lifecycle:lifecycle:method:Transition.IsDateNull", - "internal/bucket/lifecycle:lifecycle:method:Transition.IsEnabled", - "internal/bucket/lifecycle:lifecycle:method:Transition.IsNull", - "internal/bucket/lifecycle:lifecycle:method:Transition.MarshalXML", - "internal/bucket/lifecycle:lifecycle:method:Transition.NextDue", - "internal/bucket/lifecycle:lifecycle:method:Transition.UnmarshalXML", - "internal/bucket/lifecycle:lifecycle:method:Transition.Validate", - "internal/bucket/lifecycle:lifecycle:method:TransitionDate.MarshalXML", - "internal/bucket/lifecycle:lifecycle:method:TransitionDate.UnmarshalXML", - "internal/bucket/lifecycle:lifecycle:method:TransitionDays.MarshalXML", - "internal/bucket/lifecycle:lifecycle:method:TransitionDays.UnmarshalXML", - "internal/bucket/lifecycle:lifecycle:type:Action", - "internal/bucket/lifecycle:lifecycle:type:And", - "internal/bucket/lifecycle:lifecycle:type:Boolean", - "internal/bucket/lifecycle:lifecycle:type:DelMarkerExpiration", - "internal/bucket/lifecycle:lifecycle:type:Error", - "internal/bucket/lifecycle:lifecycle:type:Evaluator", - "internal/bucket/lifecycle:lifecycle:type:Event", - "internal/bucket/lifecycle:lifecycle:type:Expiration", - "internal/bucket/lifecycle:lifecycle:type:ExpirationDate", - "internal/bucket/lifecycle:lifecycle:type:ExpirationDays", - "internal/bucket/lifecycle:lifecycle:type:ExpireDeleteMarker", - "internal/bucket/lifecycle:lifecycle:type:Filter", - "internal/bucket/lifecycle:lifecycle:type:Lifecycle", - "internal/bucket/lifecycle:lifecycle:type:NoncurrentVersionExpiration", - "internal/bucket/lifecycle:lifecycle:type:NoncurrentVersionTransition", - "internal/bucket/lifecycle:lifecycle:type:ObjectOpts", - "internal/bucket/lifecycle:lifecycle:type:Prefix", - "internal/bucket/lifecycle:lifecycle:type:Rule", - "internal/bucket/lifecycle:lifecycle:type:Status", - "internal/bucket/lifecycle:lifecycle:type:Tag", - "internal/bucket/lifecycle:lifecycle:type:Transition", - "internal/bucket/lifecycle:lifecycle:type:TransitionDate", - "internal/bucket/lifecycle:lifecycle:type:TransitionDays", - "internal/bucket/object/lock:lock:const:AmzObjectLockBypassRetGovernance", - "internal/bucket/object/lock:lock:const:AmzObjectLockLegalHold", - "internal/bucket/object/lock:lock:const:AmzObjectLockMode", - "internal/bucket/object/lock:lock:const:AmzObjectLockRetainUntilDate", - "internal/bucket/object/lock:lock:const:Enabled", - "internal/bucket/object/lock:lock:const:LegalHoldOff", - "internal/bucket/object/lock:lock:const:LegalHoldOn", - "internal/bucket/object/lock:lock:const:RetCompliance", - "internal/bucket/object/lock:lock:const:RetGovernance", - "internal/bucket/object/lock:lock:field:Config.ObjectLockEnabled", - "internal/bucket/object/lock:lock:field:Config.Rule", - "internal/bucket/object/lock:lock:field:Config.XMLNS", - "internal/bucket/object/lock:lock:field:Config.XMLName", - "internal/bucket/object/lock:lock:field:DefaultRetention.Days", - "internal/bucket/object/lock:lock:field:DefaultRetention.Mode", - "internal/bucket/object/lock:lock:field:DefaultRetention.XMLName", - "internal/bucket/object/lock:lock:field:DefaultRetention.Years", - "internal/bucket/object/lock:lock:field:ObjectLegalHold.Status", - "internal/bucket/object/lock:lock:field:ObjectLegalHold.XMLNS", - "internal/bucket/object/lock:lock:field:ObjectLegalHold.XMLName", - "internal/bucket/object/lock:lock:field:ObjectRetention.Mode", - "internal/bucket/object/lock:lock:field:ObjectRetention.RetainUntilDate", - "internal/bucket/object/lock:lock:field:ObjectRetention.XMLNS", - "internal/bucket/object/lock:lock:field:ObjectRetention.XMLName", - "internal/bucket/object/lock:lock:field:Retention.LockEnabled", - "internal/bucket/object/lock:lock:field:Retention.Mode", - "internal/bucket/object/lock:lock:field:Retention.Validity", - "internal/bucket/object/lock:lock:func:FilterObjectLockMetadata", - "internal/bucket/object/lock:lock:func:GetObjectLegalHoldMeta", - "internal/bucket/object/lock:lock:func:GetObjectRetentionMeta", - "internal/bucket/object/lock:lock:func:IsObjectLockGovernanceBypassSet", - "internal/bucket/object/lock:lock:func:IsObjectLockLegalHoldRequested", - "internal/bucket/object/lock:lock:func:IsObjectLockRequested", - "internal/bucket/object/lock:lock:func:IsObjectLockRetentionRequested", - "internal/bucket/object/lock:lock:func:NewObjectLockConfig", - "internal/bucket/object/lock:lock:func:ParseObjectLegalHold", - "internal/bucket/object/lock:lock:func:ParseObjectLockConfig", - "internal/bucket/object/lock:lock:func:ParseObjectLockLegalHoldHeaders", - "internal/bucket/object/lock:lock:func:ParseObjectLockRetentionHeaders", - "internal/bucket/object/lock:lock:func:ParseObjectRetention", - "internal/bucket/object/lock:lock:func:UTCNowNTP", - "internal/bucket/object/lock:lock:method:Config.Enabled", - "internal/bucket/object/lock:lock:method:Config.String", - "internal/bucket/object/lock:lock:method:Config.ToRetention", - "internal/bucket/object/lock:lock:method:Config.UnmarshalXML", - "internal/bucket/object/lock:lock:method:DefaultRetention.UnmarshalXML", - "internal/bucket/object/lock:lock:method:LegalHoldStatus.Valid", - "internal/bucket/object/lock:lock:method:ObjectLegalHold.IsEmpty", - "internal/bucket/object/lock:lock:method:ObjectLegalHold.UnmarshalXML", - "internal/bucket/object/lock:lock:method:ObjectRetention.String", - "internal/bucket/object/lock:lock:method:RetMode.Valid", - "internal/bucket/object/lock:lock:method:Retention.Retain", - "internal/bucket/object/lock:lock:method:RetentionDate.MarshalXML", - "internal/bucket/object/lock:lock:method:RetentionDate.UnmarshalXML", - "internal/bucket/object/lock:lock:type:Config", - "internal/bucket/object/lock:lock:type:DefaultRetention", - "internal/bucket/object/lock:lock:type:LegalHoldStatus", - "internal/bucket/object/lock:lock:type:ObjectLegalHold", - "internal/bucket/object/lock:lock:type:ObjectRetention", - "internal/bucket/object/lock:lock:type:RetMode", - "internal/bucket/object/lock:lock:type:Retention", - "internal/bucket/object/lock:lock:type:RetentionDate", - "internal/bucket/object/lock:lock:var:ErrInvalidRetentionDate", - "internal/bucket/object/lock:lock:var:ErrMalformedBucketObjectConfig", - "internal/bucket/object/lock:lock:var:ErrMalformedXML", - "internal/bucket/object/lock:lock:var:ErrObjectLockInvalidHeaders", - "internal/bucket/object/lock:lock:var:ErrObjectLockMissingContentMD5", - "internal/bucket/object/lock:lock:var:ErrPastObjectLockRetainDate", - "internal/bucket/object/lock:lock:var:ErrUnknownWORMModeDirective", - "internal/bucket/replication:replication:const:AllReplicationType", - "internal/bucket/replication:replication:const:Completed", - "internal/bucket/replication:replication:const:CompletedLegacy", - "internal/bucket/replication:replication:const:DeleteReplicationType", - "internal/bucket/replication:replication:const:DestinationARNMinIOPrefix", - "internal/bucket/replication:replication:const:DestinationARNPrefix", - "internal/bucket/replication:replication:const:Disabled", - "internal/bucket/replication:replication:const:Enabled", - "internal/bucket/replication:replication:const:ExistingObjectReplicationType", - "internal/bucket/replication:replication:const:Failed", - "internal/bucket/replication:replication:const:HealReplicationType", - "internal/bucket/replication:replication:const:MetadataReplicationType", - "internal/bucket/replication:replication:const:ObjectReplicationType", - "internal/bucket/replication:replication:const:Pending", - "internal/bucket/replication:replication:const:Replica", - "internal/bucket/replication:replication:const:ResyncReplicationType", - "internal/bucket/replication:replication:const:UnsetReplicationType", - "internal/bucket/replication:replication:const:VersionPurgeComplete", - "internal/bucket/replication:replication:const:VersionPurgeFailed", - "internal/bucket/replication:replication:const:VersionPurgePending", - "internal/bucket/replication:replication:field:And.Prefix", - "internal/bucket/replication:replication:field:And.Tags", - "internal/bucket/replication:replication:field:And.XMLName", - "internal/bucket/replication:replication:field:Config.RoleArn", - "internal/bucket/replication:replication:field:Config.Rules", - "internal/bucket/replication:replication:field:Config.XMLName", - "internal/bucket/replication:replication:field:DeleteMarkerReplication.Status", - "internal/bucket/replication:replication:field:DeleteReplication.Status", - "internal/bucket/replication:replication:field:Destination.ARN", - "internal/bucket/replication:replication:field:Destination.Bucket", - "internal/bucket/replication:replication:field:Destination.StorageClass", - "internal/bucket/replication:replication:field:Destination.XMLName", - "internal/bucket/replication:replication:field:ExistingObjectReplication.Status", - "internal/bucket/replication:replication:field:Filter.And", - "internal/bucket/replication:replication:field:Filter.Prefix", - "internal/bucket/replication:replication:field:Filter.Tag", - "internal/bucket/replication:replication:field:Filter.XMLName", - "internal/bucket/replication:replication:field:ObjectOpts.DeleteMarker", - "internal/bucket/replication:replication:field:ObjectOpts.ExistingObject", - "internal/bucket/replication:replication:field:ObjectOpts.Name", - "internal/bucket/replication:replication:field:ObjectOpts.OpType", - "internal/bucket/replication:replication:field:ObjectOpts.Replica", - "internal/bucket/replication:replication:field:ObjectOpts.SSEC", - "internal/bucket/replication:replication:field:ObjectOpts.TargetArn", - "internal/bucket/replication:replication:field:ObjectOpts.UserTags", - "internal/bucket/replication:replication:field:ObjectOpts.VersionID", - "internal/bucket/replication:replication:field:ReplicaModifications.Status", - "internal/bucket/replication:replication:field:Rule.DeleteMarkerReplication", - "internal/bucket/replication:replication:field:Rule.DeleteReplication", - "internal/bucket/replication:replication:field:Rule.Destination", - "internal/bucket/replication:replication:field:Rule.ExistingObjectReplication", - "internal/bucket/replication:replication:field:Rule.Filter", - "internal/bucket/replication:replication:field:Rule.ID", - "internal/bucket/replication:replication:field:Rule.Priority", - "internal/bucket/replication:replication:field:Rule.SourceSelectionCriteria", - "internal/bucket/replication:replication:field:Rule.Status", - "internal/bucket/replication:replication:field:Rule.XMLName", - "internal/bucket/replication:replication:field:SourceSelectionCriteria.ReplicaModifications", - "internal/bucket/replication:replication:field:Tag.Key", - "internal/bucket/replication:replication:field:Tag.Value", - "internal/bucket/replication:replication:field:Tag.XMLName", - "internal/bucket/replication:replication:func:Errorf", - "internal/bucket/replication:replication:func:ParseConfig", - "internal/bucket/replication:replication:method:And.ContainsDuplicateTag", - "internal/bucket/replication:replication:method:And.Validate", - "internal/bucket/replication:replication:method:Config.FilterActionableRules", - "internal/bucket/replication:replication:method:Config.FilterTargetArns", - "internal/bucket/replication:replication:method:Config.GetDestination", - "internal/bucket/replication:replication:method:Config.HasActiveRules", - "internal/bucket/replication:replication:method:Config.HasExistingObjectReplication", - "internal/bucket/replication:replication:method:Config.Replicate", - "internal/bucket/replication:replication:method:Config.Validate", - "internal/bucket/replication:replication:method:DeleteMarkerReplication.IsEmpty", - "internal/bucket/replication:replication:method:DeleteMarkerReplication.Validate", - "internal/bucket/replication:replication:method:DeleteReplication.IsEmpty", - "internal/bucket/replication:replication:method:DeleteReplication.UnmarshalXML", - "internal/bucket/replication:replication:method:DeleteReplication.Validate", - "internal/bucket/replication:replication:method:Destination.IsValid", - "internal/bucket/replication:replication:method:Destination.LegacyArn", - "internal/bucket/replication:replication:method:Destination.MarshalXML", - "internal/bucket/replication:replication:method:Destination.String", - "internal/bucket/replication:replication:method:Destination.TargetArn", - "internal/bucket/replication:replication:method:Destination.UnmarshalXML", - "internal/bucket/replication:replication:method:Destination.Validate", - "internal/bucket/replication:replication:method:Error.Error", - "internal/bucket/replication:replication:method:Error.Unwrap", - "internal/bucket/replication:replication:method:ExistingObjectReplication.IsEmpty", - "internal/bucket/replication:replication:method:ExistingObjectReplication.UnmarshalXML", - "internal/bucket/replication:replication:method:ExistingObjectReplication.Validate", - "internal/bucket/replication:replication:method:Filter.IsEmpty", - "internal/bucket/replication:replication:method:Filter.MarshalXML", - "internal/bucket/replication:replication:method:Filter.TestTags", - "internal/bucket/replication:replication:method:Filter.Validate", - "internal/bucket/replication:replication:method:Rule.MetadataReplicate", - "internal/bucket/replication:replication:method:Rule.Prefix", - "internal/bucket/replication:replication:method:Rule.Tags", - "internal/bucket/replication:replication:method:Rule.Validate", - "internal/bucket/replication:replication:method:SourceSelectionCriteria.IsValid", - "internal/bucket/replication:replication:method:SourceSelectionCriteria.MarshalXML", - "internal/bucket/replication:replication:method:SourceSelectionCriteria.UnmarshalXML", - "internal/bucket/replication:replication:method:SourceSelectionCriteria.Validate", - "internal/bucket/replication:replication:method:StatusType.DecodeMsg", - "internal/bucket/replication:replication:method:StatusType.Empty", - "internal/bucket/replication:replication:method:StatusType.EncodeMsg", - "internal/bucket/replication:replication:method:StatusType.MarshalMsg", - "internal/bucket/replication:replication:method:StatusType.Msgsize", - "internal/bucket/replication:replication:method:StatusType.String", - "internal/bucket/replication:replication:method:StatusType.UnmarshalMsg", - "internal/bucket/replication:replication:method:Tag.IsEmpty", - "internal/bucket/replication:replication:method:Tag.String", - "internal/bucket/replication:replication:method:Tag.Validate", - "internal/bucket/replication:replication:method:Type.DecodeMsg", - "internal/bucket/replication:replication:method:Type.EncodeMsg", - "internal/bucket/replication:replication:method:Type.IsDataReplication", - "internal/bucket/replication:replication:method:Type.MarshalMsg", - "internal/bucket/replication:replication:method:Type.Msgsize", - "internal/bucket/replication:replication:method:Type.UnmarshalMsg", - "internal/bucket/replication:replication:method:Type.Valid", - "internal/bucket/replication:replication:method:VersionPurgeStatusType.DecodeMsg", - "internal/bucket/replication:replication:method:VersionPurgeStatusType.Empty", - "internal/bucket/replication:replication:method:VersionPurgeStatusType.EncodeMsg", - "internal/bucket/replication:replication:method:VersionPurgeStatusType.MarshalMsg", - "internal/bucket/replication:replication:method:VersionPurgeStatusType.Msgsize", - "internal/bucket/replication:replication:method:VersionPurgeStatusType.Pending", - "internal/bucket/replication:replication:method:VersionPurgeStatusType.UnmarshalMsg", - "internal/bucket/replication:replication:type:And", - "internal/bucket/replication:replication:type:Config", - "internal/bucket/replication:replication:type:DeleteMarkerReplication", - "internal/bucket/replication:replication:type:DeleteReplication", - "internal/bucket/replication:replication:type:Destination", - "internal/bucket/replication:replication:type:Error", - "internal/bucket/replication:replication:type:ExistingObjectReplication", - "internal/bucket/replication:replication:type:Filter", - "internal/bucket/replication:replication:type:ObjectOpts", - "internal/bucket/replication:replication:type:ReplicaModifications", - "internal/bucket/replication:replication:type:Rule", - "internal/bucket/replication:replication:type:SourceSelectionCriteria", - "internal/bucket/replication:replication:type:Status", - "internal/bucket/replication:replication:type:StatusType", - "internal/bucket/replication:replication:type:Tag", - "internal/bucket/replication:replication:type:Type", - "internal/bucket/replication:replication:type:VersionPurgeStatusType", - "internal/bucket/versioning:versioning:const:Enabled", - "internal/bucket/versioning:versioning:const:Suspended", - "internal/bucket/versioning:versioning:field:ExcludedPrefix.Prefix", - "internal/bucket/versioning:versioning:field:Versioning.ExcludeFolders", - "internal/bucket/versioning:versioning:field:Versioning.ExcludedPrefixes", - "internal/bucket/versioning:versioning:field:Versioning.Status", - "internal/bucket/versioning:versioning:field:Versioning.XMLNS", - "internal/bucket/versioning:versioning:field:Versioning.XMLName", - "internal/bucket/versioning:versioning:func:Errorf", - "internal/bucket/versioning:versioning:func:ParseConfig", - "internal/bucket/versioning:versioning:method:Error.Error", - "internal/bucket/versioning:versioning:method:Error.Unwrap", - "internal/bucket/versioning:versioning:method:Versioning.Enabled", - "internal/bucket/versioning:versioning:method:Versioning.PrefixEnabled", - "internal/bucket/versioning:versioning:method:Versioning.PrefixSuspended", - "internal/bucket/versioning:versioning:method:Versioning.PrefixesExcluded", - "internal/bucket/versioning:versioning:method:Versioning.Suspended", - "internal/bucket/versioning:versioning:method:Versioning.Validate", - "internal/bucket/versioning:versioning:method:Versioning.Versioned", - "internal/bucket/versioning:versioning:type:Error", - "internal/bucket/versioning:versioning:type:ExcludedPrefix", - "internal/bucket/versioning:versioning:type:State", - "internal/bucket/versioning:versioning:type:Versioning", - "internal/cachevalue:cachevalue:field:Cache.Once", - "internal/cachevalue:cachevalue:field:Opts.NoWait", - "internal/cachevalue:cachevalue:field:Opts.ReturnLastGood", - "internal/cachevalue:cachevalue:func:New", - "internal/cachevalue:cachevalue:func:NewFromFunc", - "internal/cachevalue:cachevalue:method:Cache.Get", - "internal/cachevalue:cachevalue:method:Cache.GetWithCtx", - "internal/cachevalue:cachevalue:method:Cache.InitOnce", - "internal/cachevalue:cachevalue:type:Cache", - "internal/cachevalue:cachevalue:type:Opts", - "internal/color:color:var:BgRed", - "internal/color:color:var:BgYellow", - "internal/color:color:var:Black", - "internal/color:color:var:Blue", - "internal/color:color:var:BlueBold", - "internal/color:color:var:Bold", - "internal/color:color:var:CyanBold", - "internal/color:color:var:FgRed", - "internal/color:color:var:FgWhite", - "internal/color:color:var:Green", - "internal/color:color:var:GreenBold", - "internal/color:color:var:Greenf", - "internal/color:color:var:IsTerminal", - "internal/color:color:var:Red", - "internal/color:color:var:RedBold", - "internal/color:color:var:RedBoldf", - "internal/color:color:var:TurnOff", - "internal/color:color:var:TurnOn", - "internal/color:color:var:Yellow", - "internal/color:color:var:YellowBold", - "internal/config/api:api:const:EnvAPIClusterDeadline", - "internal/config/api:api:const:EnvAPICorsAllowOrigin", - "internal/config/api:api:const:EnvAPIDeleteCleanupInterval", - "internal/config/api:api:const:EnvAPIDisableODirect", - "internal/config/api:api:const:EnvAPIGzipObjects", - "internal/config/api:api:const:EnvAPIListQuorum", - "internal/config/api:api:const:EnvAPIODirect", - "internal/config/api:api:const:EnvAPIObjectMaxVersions", - "internal/config/api:api:const:EnvAPIObjectMaxVersionsLegacy", - "internal/config/api:api:const:EnvAPIRemoteTransportDeadline", - "internal/config/api:api:const:EnvAPIReplicationMaxLWorkers", - "internal/config/api:api:const:EnvAPIReplicationMaxWorkers", - "internal/config/api:api:const:EnvAPIReplicationPriority", - "internal/config/api:api:const:EnvAPIRequestsDeadline", - "internal/config/api:api:const:EnvAPIRequestsMax", - "internal/config/api:api:const:EnvAPIRootAccess", - "internal/config/api:api:const:EnvAPISecureCiphers", - "internal/config/api:api:const:EnvAPIStaleUploadsCleanupInterval", - "internal/config/api:api:const:EnvAPIStaleUploadsExpiry", - "internal/config/api:api:const:EnvAPISyncEvents", - "internal/config/api:api:const:EnvAPITransitionWorkers", - "internal/config/api:api:const:EnvDeleteCleanupInterval", - "internal/config/api:api:field:Config.ClusterDeadline", - "internal/config/api:api:field:Config.CorsAllowOrigin", - "internal/config/api:api:field:Config.DeleteCleanupInterval", - "internal/config/api:api:field:Config.EnableODirect", - "internal/config/api:api:field:Config.GzipObjects", - "internal/config/api:api:field:Config.ListQuorum", - "internal/config/api:api:field:Config.ObjectMaxVersions", - "internal/config/api:api:field:Config.RemoteTransportDeadline", - "internal/config/api:api:field:Config.ReplicationMaxLWorkers", - "internal/config/api:api:field:Config.ReplicationMaxWorkers", - "internal/config/api:api:field:Config.ReplicationPriority", - "internal/config/api:api:field:Config.RequestsMax", - "internal/config/api:api:field:Config.RootAccess", - "internal/config/api:api:field:Config.StaleUploadsCleanupInterval", - "internal/config/api:api:field:Config.StaleUploadsExpiry", - "internal/config/api:api:field:Config.SyncEvents", - "internal/config/api:api:field:Config.TransitionWorkers", - "internal/config/api:api:func:LookupConfig", - "internal/config/api:api:method:Config.UnmarshalJSON", - "internal/config/api:api:type:Config", - "internal/config/api:api:var:DefaultKVS", - "internal/config/api:api:var:Help", - "internal/config/batch:batch:const:EnvKeyExpirationWorkersWait", - "internal/config/batch:batch:const:EnvKeyRotationWorkersWait", - "internal/config/batch:batch:const:EnvReplicationWorkersWait", - "internal/config/batch:batch:const:ExpirationWorkersWait", - "internal/config/batch:batch:const:KeyRotationWorkersWait", - "internal/config/batch:batch:const:ReplicationWorkersWait", - "internal/config/batch:batch:field:Config.ExpirationWorkersWait", - "internal/config/batch:batch:field:Config.KeyRotationWorkersWait", - "internal/config/batch:batch:field:Config.ReplicationWorkersWait", - "internal/config/batch:batch:func:LookupConfig", - "internal/config/batch:batch:method:Config.Clone", - "internal/config/batch:batch:method:Config.ExpirationWait", - "internal/config/batch:batch:method:Config.KeyRotationWait", - "internal/config/batch:batch:method:Config.ReplicationWait", - "internal/config/batch:batch:method:Config.Update", - "internal/config/batch:batch:type:Config", - "internal/config/batch:batch:var:DefaultKVS", - "internal/config/batch:batch:var:Help", - "internal/config/browser:browser:const:EnvBrowserCSPPolicy", - "internal/config/browser:browser:const:EnvBrowserHSTSIncludeSubdomains", - "internal/config/browser:browser:const:EnvBrowserHSTSPreload", - "internal/config/browser:browser:const:EnvBrowserHSTSSeconds", - "internal/config/browser:browser:const:EnvBrowserReferrerPolicy", - "internal/config/browser:browser:field:Config.CSPPolicy", - "internal/config/browser:browser:field:Config.HSTSIncludeSubdomains", - "internal/config/browser:browser:field:Config.HSTSPreload", - "internal/config/browser:browser:field:Config.HSTSSeconds", - "internal/config/browser:browser:field:Config.ReferrerPolicy", - "internal/config/browser:browser:func:LookupConfig", - "internal/config/browser:browser:method:Config.GetCSPolicy", - "internal/config/browser:browser:method:Config.GetHSTSSeconds", - "internal/config/browser:browser:method:Config.GetReferPolicy", - "internal/config/browser:browser:method:Config.IsHSTSIncludeSubdomains", - "internal/config/browser:browser:method:Config.IsHSTSPreload", - "internal/config/browser:browser:method:Config.Update", - "internal/config/browser:browser:type:Config", - "internal/config/browser:browser:var:DefaultKVS", - "internal/config/browser:browser:var:Help", - "internal/config/callhome:callhome:const:Enable", - "internal/config/callhome:callhome:const:Frequency", - "internal/config/callhome:callhome:field:Config.Enable", - "internal/config/callhome:callhome:field:Config.Frequency", - "internal/config/callhome:callhome:func:LookupConfig", - "internal/config/callhome:callhome:method:Config.Enabled", - "internal/config/callhome:callhome:method:Config.FrequencyDur", - "internal/config/callhome:callhome:method:Config.Update", - "internal/config/callhome:callhome:type:Config", - "internal/config/callhome:callhome:var:DefaultKVS", - "internal/config/callhome:callhome:var:HelpCallhome", - "internal/config/compress:compress:const:AllowEncrypted", - "internal/config/compress:compress:const:DefaultExtensions", - "internal/config/compress:compress:const:DefaultMimeTypes", - "internal/config/compress:compress:const:EnvCompress", - "internal/config/compress:compress:const:EnvCompressAllowEncryption", - "internal/config/compress:compress:const:EnvCompressAllowEncryptionLegacy", - "internal/config/compress:compress:const:EnvCompressEnableLegacy", - "internal/config/compress:compress:const:EnvCompressExtensions", - "internal/config/compress:compress:const:EnvCompressExtensionsLegacy", - "internal/config/compress:compress:const:EnvCompressMimeTypes", - "internal/config/compress:compress:const:EnvCompressMimeTypesLegacy1", - "internal/config/compress:compress:const:EnvCompressMimeTypesLegacy2", - "internal/config/compress:compress:const:EnvCompressState", - "internal/config/compress:compress:const:Extensions", - "internal/config/compress:compress:const:MimeTypes", - "internal/config/compress:compress:field:Config.AllowEncrypted", - "internal/config/compress:compress:field:Config.Enabled", - "internal/config/compress:compress:field:Config.Extensions", - "internal/config/compress:compress:field:Config.MimeTypes", - "internal/config/compress:compress:func:LookupConfig", - "internal/config/compress:compress:func:SetCompressionConfig", - "internal/config/compress:compress:type:Config", - "internal/config/compress:compress:var:DefaultKVS", - "internal/config/compress:compress:var:Help", - "internal/config/dns:dns:field:Error.Bucket", - "internal/config/dns:dns:field:Error.Err", - "internal/config/dns:dns:field:OperatorDNS.Endpoint", - "internal/config/dns:dns:field:SrvRecord.CreationDate", - "internal/config/dns:dns:field:SrvRecord.Group", - "internal/config/dns:dns:field:SrvRecord.Host", - "internal/config/dns:dns:field:SrvRecord.Key", - "internal/config/dns:dns:field:SrvRecord.Mail", - "internal/config/dns:dns:field:SrvRecord.Port", - "internal/config/dns:dns:field:SrvRecord.Priority", - "internal/config/dns:dns:field:SrvRecord.TTL", - "internal/config/dns:dns:field:SrvRecord.TargetStrip", - "internal/config/dns:dns:field:SrvRecord.Text", - "internal/config/dns:dns:field:SrvRecord.Weight", - "internal/config/dns:dns:field:Store.Close", - "internal/config/dns:dns:field:Store.Delete", - "internal/config/dns:dns:field:Store.DeleteRecord", - "internal/config/dns:dns:field:Store.Get", - "internal/config/dns:dns:field:Store.List", - "internal/config/dns:dns:field:Store.Put", - "internal/config/dns:dns:field:Store.String", - "internal/config/dns:dns:func:Authentication", - "internal/config/dns:dns:func:CoreDNSPath", - "internal/config/dns:dns:func:DomainIPs", - "internal/config/dns:dns:func:DomainNames", - "internal/config/dns:dns:func:DomainPort", - "internal/config/dns:dns:func:NewCoreDNS", - "internal/config/dns:dns:func:NewOperatorDNS", - "internal/config/dns:dns:func:RootCAs", - "internal/config/dns:dns:method:CoreDNS.Close", - "internal/config/dns:dns:method:CoreDNS.Delete", - "internal/config/dns:dns:method:CoreDNS.DeleteRecord", - "internal/config/dns:dns:method:CoreDNS.Get", - "internal/config/dns:dns:method:CoreDNS.List", - "internal/config/dns:dns:method:CoreDNS.Put", - "internal/config/dns:dns:method:CoreDNS.String", - "internal/config/dns:dns:method:ErrBucketConflict.Error", - "internal/config/dns:dns:method:ErrInvalidBucketName.Error", - "internal/config/dns:dns:method:Error.Error", - "internal/config/dns:dns:method:OperatorDNS.Close", - "internal/config/dns:dns:method:OperatorDNS.Delete", - "internal/config/dns:dns:method:OperatorDNS.DeleteRecord", - "internal/config/dns:dns:method:OperatorDNS.Get", - "internal/config/dns:dns:method:OperatorDNS.List", - "internal/config/dns:dns:method:OperatorDNS.Put", - "internal/config/dns:dns:method:OperatorDNS.String", - "internal/config/dns:dns:type:CoreDNS", - "internal/config/dns:dns:type:ErrBucketConflict", - "internal/config/dns:dns:type:ErrInvalidBucketName", - "internal/config/dns:dns:type:Error", - "internal/config/dns:dns:type:EtcdOption", - "internal/config/dns:dns:type:OperatorDNS", - "internal/config/dns:dns:type:OperatorOption", - "internal/config/dns:dns:type:SrvRecord", - "internal/config/dns:dns:type:Store", - "internal/config/dns:dns:var:ErrDomainMissing", - "internal/config/dns:dns:var:ErrNoEntriesFound", - "internal/config/dns:dns:var:ErrNotImplemented", - "internal/config/drive:drive:const:EnvMaxDiskTimeoutLegacy", - "internal/config/drive:drive:const:EnvMaxDriveTimeout", - "internal/config/drive:drive:const:EnvMaxDriveTimeoutLegacy", - "internal/config/drive:drive:field:Config.MaxTimeout", - "internal/config/drive:drive:func:LookupConfig", - "internal/config/drive:drive:method:Config.GetMaxTimeout", - "internal/config/drive:drive:method:Config.GetOPTimeout", - "internal/config/drive:drive:method:Config.Update", - "internal/config/drive:drive:type:Config", - "internal/config/drive:drive:var:DefaultKVS", - "internal/config/drive:drive:var:HelpDrive", - "internal/config/drive:drive:var:MaxTimeout", - "internal/config/etcd:etcd:const:ClientCert", - "internal/config/etcd:etcd:const:ClientCertKey", - "internal/config/etcd:etcd:const:CoreDNSPath", - "internal/config/etcd:etcd:const:Endpoints", - "internal/config/etcd:etcd:const:EnvEtcdClientCert", - "internal/config/etcd:etcd:const:EnvEtcdClientCertKey", - "internal/config/etcd:etcd:const:EnvEtcdCoreDNSPath", - "internal/config/etcd:etcd:const:EnvEtcdEndpoints", - "internal/config/etcd:etcd:const:EnvEtcdPathPrefix", - "internal/config/etcd:etcd:const:PathPrefix", - "internal/config/etcd:etcd:field:Config.CoreDNSPath", - "internal/config/etcd:etcd:field:Config.Enabled", - "internal/config/etcd:etcd:field:Config.PathPrefix", - "internal/config/etcd:etcd:func:Enabled", - "internal/config/etcd:etcd:func:LookupConfig", - "internal/config/etcd:etcd:func:New", - "internal/config/etcd:etcd:type:Config", - "internal/config/etcd:etcd:var:DefaultKVS", - "internal/config/etcd:etcd:var:Help", - "internal/config/heal:heal:const:Bitrot", - "internal/config/heal:heal:const:DriveWorkers", - "internal/config/heal:heal:const:EnvBitrot", - "internal/config/heal:heal:const:EnvDriveWorkers", - "internal/config/heal:heal:const:EnvIOCount", - "internal/config/heal:heal:const:EnvSleep", - "internal/config/heal:heal:const:IOCount", - "internal/config/heal:heal:const:Sleep", - "internal/config/heal:heal:field:Config.Bitrot", - "internal/config/heal:heal:field:Config.DriveWorkers", - "internal/config/heal:heal:field:Config.IOCount", - "internal/config/heal:heal:field:Config.Sleep", - "internal/config/heal:heal:func:LookupConfig", - "internal/config/heal:heal:method:Config.BitrotScanCycle", - "internal/config/heal:heal:method:Config.Clone", - "internal/config/heal:heal:method:Config.GetWorkers", - "internal/config/heal:heal:method:Config.Update", - "internal/config/heal:heal:type:Config", - "internal/config/heal:heal:var:DefaultKVS", - "internal/config/heal:heal:var:Help", - "internal/config/identity/ldap:ldap:const:EnvGroupSearchBaseDN", - "internal/config/identity/ldap:ldap:const:EnvGroupSearchFilter", - "internal/config/identity/ldap:ldap:const:EnvLookupBindDN", - "internal/config/identity/ldap:ldap:const:EnvLookupBindPassword", - "internal/config/identity/ldap:ldap:const:EnvSRVRecordName", - "internal/config/identity/ldap:ldap:const:EnvSTSTrustedProxies", - "internal/config/identity/ldap:ldap:const:EnvServerAddr", - "internal/config/identity/ldap:ldap:const:EnvServerInsecure", - "internal/config/identity/ldap:ldap:const:EnvServerStartTLS", - "internal/config/identity/ldap:ldap:const:EnvTLSSkipVerify", - "internal/config/identity/ldap:ldap:const:EnvUserDNAttributes", - "internal/config/identity/ldap:ldap:const:EnvUserDNSearchBaseDN", - "internal/config/identity/ldap:ldap:const:EnvUserDNSearchFilter", - "internal/config/identity/ldap:ldap:const:EnvUsernameFormat", - "internal/config/identity/ldap:ldap:const:GroupSearchBaseDN", - "internal/config/identity/ldap:ldap:const:GroupSearchFilter", - "internal/config/identity/ldap:ldap:const:LookupBindDN", - "internal/config/identity/ldap:ldap:const:LookupBindPassword", - "internal/config/identity/ldap:ldap:const:SRVRecordName", - "internal/config/identity/ldap:ldap:const:STSTrustedProxies", - "internal/config/identity/ldap:ldap:const:ServerAddr", - "internal/config/identity/ldap:ldap:const:ServerInsecure", - "internal/config/identity/ldap:ldap:const:ServerStartTLS", - "internal/config/identity/ldap:ldap:const:TLSSkipVerify", - "internal/config/identity/ldap:ldap:const:UserDNAttributes", - "internal/config/identity/ldap:ldap:const:UserDNSearchBaseDN", - "internal/config/identity/ldap:ldap:const:UserDNSearchFilter", - "internal/config/identity/ldap:ldap:field:Config.LDAP", - "internal/config/identity/ldap:ldap:field:LegacyConfig.Enabled", - "internal/config/identity/ldap:ldap:field:LegacyConfig.GroupSearchBaseDistName", - "internal/config/identity/ldap:ldap:field:LegacyConfig.GroupSearchBaseDistNames", - "internal/config/identity/ldap:ldap:field:LegacyConfig.GroupSearchFilter", - "internal/config/identity/ldap:ldap:field:LegacyConfig.LookupBindDN", - "internal/config/identity/ldap:ldap:field:LegacyConfig.LookupBindPassword", - "internal/config/identity/ldap:ldap:field:LegacyConfig.ServerAddr", - "internal/config/identity/ldap:ldap:field:LegacyConfig.UserDNSearchBaseDistName", - "internal/config/identity/ldap:ldap:field:LegacyConfig.UserDNSearchBaseDistNames", - "internal/config/identity/ldap:ldap:field:LegacyConfig.UserDNSearchFilter", - "internal/config/identity/ldap:ldap:func:Enabled", - "internal/config/identity/ldap:ldap:func:IsAuthError", - "internal/config/identity/ldap:ldap:func:Lookup", - "internal/config/identity/ldap:ldap:func:SetIdentityLDAP", - "internal/config/identity/ldap:ldap:method:Config.Bind", - "internal/config/identity/ldap:ldap:method:Config.Clone", - "internal/config/identity/ldap:ldap:method:Config.DecodeDN", - "internal/config/identity/ldap:ldap:method:Config.Enabled", - "internal/config/identity/ldap:ldap:method:Config.GetConfigInfo", - "internal/config/identity/ldap:ldap:method:Config.GetConfigList", - "internal/config/identity/ldap:ldap:method:Config.GetExpiryDuration", - "internal/config/identity/ldap:ldap:method:Config.GetNonEligibleUserDistNames", - "internal/config/identity/ldap:ldap:method:Config.GetValidatedDNForUsername", - "internal/config/identity/ldap:ldap:method:Config.GetValidatedDNUnderBaseDN", - "internal/config/identity/ldap:ldap:method:Config.GetValidatedDNWithGroups", - "internal/config/identity/ldap:ldap:method:Config.GetValidatedGroupDN", - "internal/config/identity/ldap:ldap:method:Config.GetValidatedUserDN", - "internal/config/identity/ldap:ldap:method:Config.IsLDAPGroupDN", - "internal/config/identity/ldap:ldap:method:Config.IsLDAPUserDN", - "internal/config/identity/ldap:ldap:method:Config.IsSTSTrustedProxy", - "internal/config/identity/ldap:ldap:method:Config.LookupGroupMemberships", - "internal/config/identity/ldap:ldap:method:Config.LookupUserDN", - "internal/config/identity/ldap:ldap:method:Config.ParsesAsDN", - "internal/config/identity/ldap:ldap:method:Config.QuickNormalizeDN", - "internal/config/identity/ldap:ldap:method:Config.SetSTSTrustedProxies", - "internal/config/identity/ldap:ldap:method:authError.Error", - "internal/config/identity/ldap:ldap:method:authError.Is", - "internal/config/identity/ldap:ldap:method:authError.Unwrap", - "internal/config/identity/ldap:ldap:type:Config", - "internal/config/identity/ldap:ldap:type:LegacyConfig", - "internal/config/identity/ldap:ldap:var:DefaultKVS", - "internal/config/identity/ldap:ldap:var:ErrProviderConfigNotFound", - "internal/config/identity/ldap:ldap:var:Help", - "internal/config/identity/openid/provider:provider:field:DiscoveryDoc.AuthEndpoint", - "internal/config/identity/openid/provider:provider:field:DiscoveryDoc.ClaimsSupported", - "internal/config/identity/openid/provider:provider:field:DiscoveryDoc.CodeChallengeMethodsSupported", - "internal/config/identity/openid/provider:provider:field:DiscoveryDoc.EndSessionEndpoint", - "internal/config/identity/openid/provider:provider:field:DiscoveryDoc.IDTokenSigningAlgValuesSupported", - "internal/config/identity/openid/provider:provider:field:DiscoveryDoc.Issuer", - "internal/config/identity/openid/provider:provider:field:DiscoveryDoc.JwksURI", - "internal/config/identity/openid/provider:provider:field:DiscoveryDoc.ResponseTypesSupported", - "internal/config/identity/openid/provider:provider:field:DiscoveryDoc.RevocationEndpoint", - "internal/config/identity/openid/provider:provider:field:DiscoveryDoc.ScopesSupported", - "internal/config/identity/openid/provider:provider:field:DiscoveryDoc.SubjectTypesSupported", - "internal/config/identity/openid/provider:provider:field:DiscoveryDoc.TokenEndpoint", - "internal/config/identity/openid/provider:provider:field:DiscoveryDoc.TokenEndpointAuthMethods", - "internal/config/identity/openid/provider:provider:field:DiscoveryDoc.UserInfoEndpoint", - "internal/config/identity/openid/provider:provider:field:Provider.LoginWithClientID", - "internal/config/identity/openid/provider:provider:field:Provider.LoginWithUser", - "internal/config/identity/openid/provider:provider:field:Provider.LookupUser", - "internal/config/identity/openid/provider:provider:field:Token.AccessToken", - "internal/config/identity/openid/provider:provider:field:Token.Expiry", - "internal/config/identity/openid/provider:provider:field:User.Enabled", - "internal/config/identity/openid/provider:provider:field:User.ID", - "internal/config/identity/openid/provider:provider:field:User.Name", - "internal/config/identity/openid/provider:provider:func:KeyCloak", - "internal/config/identity/openid/provider:provider:func:WithAdminURL", - "internal/config/identity/openid/provider:provider:func:WithOpenIDConfig", - "internal/config/identity/openid/provider:provider:func:WithRealm", - "internal/config/identity/openid/provider:provider:func:WithTransport", - "internal/config/identity/openid/provider:provider:method:KeycloakProvider.LoginWithClientID", - "internal/config/identity/openid/provider:provider:method:KeycloakProvider.LoginWithUser", - "internal/config/identity/openid/provider:provider:method:KeycloakProvider.LookupUser", - "internal/config/identity/openid/provider:provider:type:DiscoveryDoc", - "internal/config/identity/openid/provider:provider:type:KeycloakProvider", - "internal/config/identity/openid/provider:provider:type:Option", - "internal/config/identity/openid/provider:provider:type:Provider", - "internal/config/identity/openid/provider:provider:type:Token", - "internal/config/identity/openid/provider:provider:type:User", - "internal/config/identity/openid/provider:provider:var:ErrAccessTokenExpired", - "internal/config/identity/openid/provider:provider:var:ErrNotImplemented", - "internal/config/identity/openid:openid:const:ClaimName", - "internal/config/identity/openid:openid:const:ClaimPrefix", - "internal/config/identity/openid:openid:const:ClaimUserinfo", - "internal/config/identity/openid:openid:const:ClientID", - "internal/config/identity/openid:openid:const:ClientSecret", - "internal/config/identity/openid:openid:const:ConfigURL", - "internal/config/identity/openid:openid:const:DisplayName", - "internal/config/identity/openid:openid:const:JwksURL", - "internal/config/identity/openid:openid:const:KeyCloakAdminURL", - "internal/config/identity/openid:openid:const:KeyCloakRealm", - "internal/config/identity/openid:openid:const:RedirectURI", - "internal/config/identity/openid:openid:const:RedirectURIDynamic", - "internal/config/identity/openid:openid:const:RolePolicy", - "internal/config/identity/openid:openid:const:Scopes", - "internal/config/identity/openid:openid:const:UserIDClaim", - "internal/config/identity/openid:openid:const:UserReadableClaim", - "internal/config/identity/openid:openid:const:Vendor", - "internal/config/identity/openid:openid:field:Config.Enabled", - "internal/config/identity/openid:openid:field:Config.ProviderCfgs", - "internal/config/identity/openid:openid:field:DiscoveryDoc.AuthEndpoint", - "internal/config/identity/openid:openid:field:DiscoveryDoc.ClaimsSupported", - "internal/config/identity/openid:openid:field:DiscoveryDoc.CodeChallengeMethodsSupported", - "internal/config/identity/openid:openid:field:DiscoveryDoc.EndSessionEndpoint", - "internal/config/identity/openid:openid:field:DiscoveryDoc.IDTokenSigningAlgValuesSupported", - "internal/config/identity/openid:openid:field:DiscoveryDoc.Issuer", - "internal/config/identity/openid:openid:field:DiscoveryDoc.JwksURI", - "internal/config/identity/openid:openid:field:DiscoveryDoc.ResponseTypesSupported", - "internal/config/identity/openid:openid:field:DiscoveryDoc.RevocationEndpoint", - "internal/config/identity/openid:openid:field:DiscoveryDoc.ScopesSupported", - "internal/config/identity/openid:openid:field:DiscoveryDoc.SubjectTypesSupported", - "internal/config/identity/openid:openid:field:DiscoveryDoc.TokenEndpoint", - "internal/config/identity/openid:openid:field:DiscoveryDoc.TokenEndpointAuthMethods", - "internal/config/identity/openid:openid:field:DiscoveryDoc.UserInfoEndpoint", - "internal/config/identity/openid:openid:field:JWKS.Alg", - "internal/config/identity/openid:openid:field:JWKS.Crv", - "internal/config/identity/openid:openid:field:JWKS.D", - "internal/config/identity/openid:openid:field:JWKS.E", - "internal/config/identity/openid:openid:field:JWKS.K", - "internal/config/identity/openid:openid:field:JWKS.Keys", - "internal/config/identity/openid:openid:field:JWKS.Kid", - "internal/config/identity/openid:openid:field:JWKS.Kty", - "internal/config/identity/openid:openid:field:JWKS.N", - "internal/config/identity/openid:openid:field:JWKS.Use", - "internal/config/identity/openid:openid:field:JWKS.X", - "internal/config/identity/openid:openid:field:JWKS.Y", - "internal/config/identity/openid:openid:func:Enabled", - "internal/config/identity/openid:openid:func:GetDefaultExpiration", - "internal/config/identity/openid:openid:func:LookupConfig", - "internal/config/identity/openid:openid:method:Config.Clone", - "internal/config/identity/openid:openid:method:Config.GetConfigInfo", - "internal/config/identity/openid:openid:method:Config.GetConfigList", - "internal/config/identity/openid:openid:method:Config.GetIAMPolicyClaimName", - "internal/config/identity/openid:openid:method:Config.GetRoleInfo", - "internal/config/identity/openid:openid:method:Config.GetSettings", - "internal/config/identity/openid:openid:method:Config.GetUserIDClaim", - "internal/config/identity/openid:openid:method:Config.GetUserReadableClaim", - "internal/config/identity/openid:openid:method:Config.LookupUser", - "internal/config/identity/openid:openid:method:Config.PopulatePublicKey", - "internal/config/identity/openid:openid:method:Config.ProviderEnabled", - "internal/config/identity/openid:openid:method:Config.Validate", - "internal/config/identity/openid:openid:method:JWKS.DecodePublicKey", - "internal/config/identity/openid:openid:method:providerCfg.GetRoleArn", - "internal/config/identity/openid:openid:method:providerCfg.UserInfo", - "internal/config/identity/openid:openid:type:Config", - "internal/config/identity/openid:openid:type:DiscoveryDoc", - "internal/config/identity/openid:openid:type:JWKS", - "internal/config/identity/openid:openid:var:DefaultKVS", - "internal/config/identity/openid:openid:var:DummyRoleARN", - "internal/config/identity/openid:openid:var:ErrProviderConfigNotFound", - "internal/config/identity/openid:openid:var:ErrTokenExpired", - "internal/config/identity/openid:openid:var:Help", - "internal/config/identity/openid:openid:var:SigningMethodES3256", - "internal/config/identity/openid:openid:var:SigningMethodES3384", - "internal/config/identity/openid:openid:var:SigningMethodES3512", - "internal/config/identity/openid:openid:var:SigningMethodRS3256", - "internal/config/identity/openid:openid:var:SigningMethodRS3384", - "internal/config/identity/openid:openid:var:SigningMethodRS3512", - "internal/config/identity/plugin:plugin:const:AuthToken", - "internal/config/identity/plugin:plugin:const:EnvIdentityPluginAuthToken", - "internal/config/identity/plugin:plugin:const:EnvIdentityPluginRoleID", - "internal/config/identity/plugin:plugin:const:EnvIdentityPluginRolePolicy", - "internal/config/identity/plugin:plugin:const:EnvIdentityPluginURL", - "internal/config/identity/plugin:plugin:const:RoleID", - "internal/config/identity/plugin:plugin:const:RolePolicy", - "internal/config/identity/plugin:plugin:const:URL", - "internal/config/identity/plugin:plugin:field:Args.AuthToken", - "internal/config/identity/plugin:plugin:field:Args.CloseRespFn", - "internal/config/identity/plugin:plugin:field:Args.RoleARN", - "internal/config/identity/plugin:plugin:field:Args.RolePolicy", - "internal/config/identity/plugin:plugin:field:Args.Transport", - "internal/config/identity/plugin:plugin:field:Args.URL", - "internal/config/identity/plugin:plugin:field:AuthNErrorResponse.Reason", - "internal/config/identity/plugin:plugin:field:AuthNResponse.Failure", - "internal/config/identity/plugin:plugin:field:AuthNResponse.Success", - "internal/config/identity/plugin:plugin:field:AuthNSuccessResponse.Claims", - "internal/config/identity/plugin:plugin:field:AuthNSuccessResponse.MaxValiditySeconds", - "internal/config/identity/plugin:plugin:field:AuthNSuccessResponse.User", - "internal/config/identity/plugin:plugin:field:Metrics.AvgSuccRTTMs", - "internal/config/identity/plugin:plugin:field:Metrics.FailedRequests", - "internal/config/identity/plugin:plugin:field:Metrics.LastReachableSecs", - "internal/config/identity/plugin:plugin:field:Metrics.LastUnreachableSecs", - "internal/config/identity/plugin:plugin:field:Metrics.MaxSuccRTTMs", - "internal/config/identity/plugin:plugin:field:Metrics.TotalRequests", - "internal/config/identity/plugin:plugin:func:Enabled", - "internal/config/identity/plugin:plugin:func:LookupConfig", - "internal/config/identity/plugin:plugin:func:New", - "internal/config/identity/plugin:plugin:method:Args.Validate", - "internal/config/identity/plugin:plugin:method:AuthNPlugin.Authenticate", - "internal/config/identity/plugin:plugin:method:AuthNPlugin.GetRoleInfo", - "internal/config/identity/plugin:plugin:method:AuthNPlugin.Metrics", - "internal/config/identity/plugin:plugin:type:Args", - "internal/config/identity/plugin:plugin:type:AuthNErrorResponse", - "internal/config/identity/plugin:plugin:type:AuthNPlugin", - "internal/config/identity/plugin:plugin:type:AuthNResponse", - "internal/config/identity/plugin:plugin:type:AuthNSuccessResponse", - "internal/config/identity/plugin:plugin:type:Metrics", - "internal/config/identity/plugin:plugin:var:DefaultKVS", - "internal/config/identity/plugin:plugin:var:Help", - "internal/config/identity/tls:tls:const:EnvIdentityTLSEnabled", - "internal/config/identity/tls:tls:const:EnvIdentityTLSSkipVerify", - "internal/config/identity/tls:tls:field:Config.Enabled", - "internal/config/identity/tls:tls:field:Config.InsecureSkipVerify", - "internal/config/identity/tls:tls:func:Lookup", - "internal/config/identity/tls:tls:method:Config.GetExpiryDuration", - "internal/config/identity/tls:tls:type:Config", - "internal/config/identity/tls:tls:var:DefaultKVS", - "internal/config/identity/tls:tls:var:Help", - "internal/config/ilm:ilm:const:EnvILMExpirationWorkers", - "internal/config/ilm:ilm:const:EnvILMTransitionWorkers", - "internal/config/ilm:ilm:field:Config.ExpirationWorkers", - "internal/config/ilm:ilm:field:Config.TransitionWorkers", - "internal/config/ilm:ilm:func:LookupConfig", - "internal/config/ilm:ilm:type:Config", - "internal/config/ilm:ilm:var:DefaultKVS", - "internal/config/ilm:ilm:var:Help", - "internal/config/lambda/event:event:field:ErrARNNotFound.ARN", - "internal/config/lambda/event:event:field:ErrInvalidARN.ARN", - "internal/config/lambda/event:event:field:ErrUnknownRegion.Region", - "internal/config/lambda/event:event:field:Event.GetObjectContext", - "internal/config/lambda/event:event:field:Event.ProtocolVersion", - "internal/config/lambda/event:event:field:Event.UserIdentity", - "internal/config/lambda/event:event:field:Event.UserRequest", - "internal/config/lambda/event:event:field:GetObjectContext.InputS3URL", - "internal/config/lambda/event:event:field:GetObjectContext.OutputRoute", - "internal/config/lambda/event:event:field:GetObjectContext.OutputToken", - "internal/config/lambda/event:event:field:Identity.AccessKeyID", - "internal/config/lambda/event:event:field:Identity.PrincipalID", - "internal/config/lambda/event:event:field:Identity.Type", - "internal/config/lambda/event:event:field:Target.Close", - "internal/config/lambda/event:event:field:Target.ID", - "internal/config/lambda/event:event:field:Target.IsActive", - "internal/config/lambda/event:event:field:Target.Send", - "internal/config/lambda/event:event:field:Target.Stat", - "internal/config/lambda/event:event:field:TargetID.ID", - "internal/config/lambda/event:event:field:TargetID.Name", - "internal/config/lambda/event:event:field:TargetIDResult.Err", - "internal/config/lambda/event:event:field:TargetIDResult.ID", - "internal/config/lambda/event:event:field:TargetStat.ActiveRequests", - "internal/config/lambda/event:event:field:TargetStat.FailedRequests", - "internal/config/lambda/event:event:field:TargetStat.ID", - "internal/config/lambda/event:event:field:TargetStat.TotalRequests", - "internal/config/lambda/event:event:field:TargetStats.TargetStats", - "internal/config/lambda/event:event:field:UserRequest.Headers", - "internal/config/lambda/event:event:field:UserRequest.URL", - "internal/config/lambda/event:event:func:NewTargetIDSet", - "internal/config/lambda/event:event:func:NewTargetList", - "internal/config/lambda/event:event:func:ParseARN", - "internal/config/lambda/event:event:method:ARN.String", - "internal/config/lambda/event:event:method:ErrARNNotFound.Error", - "internal/config/lambda/event:event:method:ErrInvalidARN.Error", - "internal/config/lambda/event:event:method:ErrUnknownRegion.Error", - "internal/config/lambda/event:event:method:TargetID.MarshalJSON", - "internal/config/lambda/event:event:method:TargetID.String", - "internal/config/lambda/event:event:method:TargetID.ToARN", - "internal/config/lambda/event:event:method:TargetID.UnmarshalJSON", - "internal/config/lambda/event:event:method:TargetIDSet.Clone", - "internal/config/lambda/event:event:method:TargetIDSet.Difference", - "internal/config/lambda/event:event:method:TargetIDSet.IsEmpty", - "internal/config/lambda/event:event:method:TargetIDSet.Union", - "internal/config/lambda/event:event:method:TargetList.Add", - "internal/config/lambda/event:event:method:TargetList.Empty", - "internal/config/lambda/event:event:method:TargetList.List", - "internal/config/lambda/event:event:method:TargetList.Lookup", - "internal/config/lambda/event:event:method:TargetList.Remove", - "internal/config/lambda/event:event:method:TargetList.Send", - "internal/config/lambda/event:event:method:TargetList.Stats", - "internal/config/lambda/event:event:method:TargetList.TargetMap", - "internal/config/lambda/event:event:method:TargetList.Targets", - "internal/config/lambda/event:event:type:ARN", - "internal/config/lambda/event:event:type:ErrARNNotFound", - "internal/config/lambda/event:event:type:ErrInvalidARN", - "internal/config/lambda/event:event:type:ErrUnknownRegion", - "internal/config/lambda/event:event:type:Event", - "internal/config/lambda/event:event:type:GetObjectContext", - "internal/config/lambda/event:event:type:Identity", - "internal/config/lambda/event:event:type:Target", - "internal/config/lambda/event:event:type:TargetID", - "internal/config/lambda/event:event:type:TargetIDResult", - "internal/config/lambda/event:event:type:TargetIDSet", - "internal/config/lambda/event:event:type:TargetList", - "internal/config/lambda/event:event:type:TargetStat", - "internal/config/lambda/event:event:type:TargetStats", - "internal/config/lambda/event:event:type:UserRequest", - "internal/config/lambda/target:target:const:EnvWebhookAuthToken", - "internal/config/lambda/target:target:const:EnvWebhookClientCert", - "internal/config/lambda/target:target:const:EnvWebhookClientKey", - "internal/config/lambda/target:target:const:EnvWebhookEnable", - "internal/config/lambda/target:target:const:EnvWebhookEndpoint", - "internal/config/lambda/target:target:const:WebhookAuthToken", - "internal/config/lambda/target:target:const:WebhookClientCert", - "internal/config/lambda/target:target:const:WebhookClientKey", - "internal/config/lambda/target:target:const:WebhookEndpoint", - "internal/config/lambda/target:target:field:WebhookArgs.AuthToken", - "internal/config/lambda/target:target:field:WebhookArgs.ClientCert", - "internal/config/lambda/target:target:field:WebhookArgs.ClientKey", - "internal/config/lambda/target:target:field:WebhookArgs.Enable", - "internal/config/lambda/target:target:field:WebhookArgs.Endpoint", - "internal/config/lambda/target:target:field:WebhookArgs.Transport", - "internal/config/lambda/target:target:func:NewWebhookTarget", - "internal/config/lambda/target:target:method:WebhookArgs.Validate", - "internal/config/lambda/target:target:method:WebhookTarget.Close", - "internal/config/lambda/target:target:method:WebhookTarget.ID", - "internal/config/lambda/target:target:method:WebhookTarget.IsActive", - "internal/config/lambda/target:target:method:WebhookTarget.Send", - "internal/config/lambda/target:target:method:WebhookTarget.Stat", - "internal/config/lambda/target:target:method:lazyInit.Do", - "internal/config/lambda/target:target:type:WebhookArgs", - "internal/config/lambda/target:target:type:WebhookTarget", - "internal/config/lambda:lambda:field:Config.Webhook", - "internal/config/lambda:lambda:func:FetchEnabledTargets", - "internal/config/lambda:lambda:func:GetLambdaWebhook", - "internal/config/lambda:lambda:func:NewConfig", - "internal/config/lambda:lambda:func:TestSubSysLambdaTargets", - "internal/config/lambda:lambda:type:Config", - "internal/config/lambda:lambda:var:DefaultLambdaKVS", - "internal/config/lambda:lambda:var:DefaultWebhookKVS", - "internal/config/lambda:lambda:var:ErrTargetsOffline", - "internal/config/lambda:lambda:var:HelpWebhook", - "internal/config/notify:notify:field:Config.AMQP", - "internal/config/notify:notify:field:Config.Elasticsearch", - "internal/config/notify:notify:field:Config.Kafka", - "internal/config/notify:notify:field:Config.MQTT", - "internal/config/notify:notify:field:Config.MySQL", - "internal/config/notify:notify:field:Config.NATS", - "internal/config/notify:notify:field:Config.NSQ", - "internal/config/notify:notify:field:Config.PostgreSQL", - "internal/config/notify:notify:field:Config.Redis", - "internal/config/notify:notify:field:Config.Webhook", - "internal/config/notify:notify:func:FetchEnabledTargets", - "internal/config/notify:notify:func:GetNotifyAMQP", - "internal/config/notify:notify:func:GetNotifyES", - "internal/config/notify:notify:func:GetNotifyKafka", - "internal/config/notify:notify:func:GetNotifyMQTT", - "internal/config/notify:notify:func:GetNotifyMySQL", - "internal/config/notify:notify:func:GetNotifyNATS", - "internal/config/notify:notify:func:GetNotifyNSQ", - "internal/config/notify:notify:func:GetNotifyPostgres", - "internal/config/notify:notify:func:GetNotifyRedis", - "internal/config/notify:notify:func:GetNotifyWebhook", - "internal/config/notify:notify:func:NewConfig", - "internal/config/notify:notify:func:SetNotifyAMQP", - "internal/config/notify:notify:func:SetNotifyES", - "internal/config/notify:notify:func:SetNotifyKafka", - "internal/config/notify:notify:func:SetNotifyMQTT", - "internal/config/notify:notify:func:SetNotifyMySQL", - "internal/config/notify:notify:func:SetNotifyNATS", - "internal/config/notify:notify:func:SetNotifyNSQ", - "internal/config/notify:notify:func:SetNotifyPostgres", - "internal/config/notify:notify:func:SetNotifyRedis", - "internal/config/notify:notify:func:SetNotifyWebhook", - "internal/config/notify:notify:func:TestSubSysNotificationTargets", - "internal/config/notify:notify:type:Config", - "internal/config/notify:notify:var:DefaultAMQPKVS", - "internal/config/notify:notify:var:DefaultESKVS", - "internal/config/notify:notify:var:DefaultKafkaKVS", - "internal/config/notify:notify:var:DefaultMQTTKVS", - "internal/config/notify:notify:var:DefaultMySQLKVS", - "internal/config/notify:notify:var:DefaultNATSKVS", - "internal/config/notify:notify:var:DefaultNSQKVS", - "internal/config/notify:notify:var:DefaultNotificationKVS", - "internal/config/notify:notify:var:DefaultPostgresKVS", - "internal/config/notify:notify:var:DefaultRedisKVS", - "internal/config/notify:notify:var:DefaultWebhookKVS", - "internal/config/notify:notify:var:ErrTargetsOffline", - "internal/config/notify:notify:var:HelpAMQP", - "internal/config/notify:notify:var:HelpES", - "internal/config/notify:notify:var:HelpKafka", - "internal/config/notify:notify:var:HelpMQTT", - "internal/config/notify:notify:var:HelpMySQL", - "internal/config/notify:notify:var:HelpNATS", - "internal/config/notify:notify:var:HelpNSQ", - "internal/config/notify:notify:var:HelpPostgres", - "internal/config/notify:notify:var:HelpRedis", - "internal/config/notify:notify:var:HelpWebhook", - "internal/config/policy/opa:opa:const:AuthToken", - "internal/config/policy/opa:opa:const:EnvIamOpaAuthToken", - "internal/config/policy/opa:opa:const:EnvIamOpaURL", - "internal/config/policy/opa:opa:const:EnvPolicyOpaAuthToken", - "internal/config/policy/opa:opa:const:EnvPolicyOpaURL", - "internal/config/policy/opa:opa:const:URL", - "internal/config/policy/opa:opa:field:Args.AuthToken", - "internal/config/policy/opa:opa:field:Args.CloseRespFn", - "internal/config/policy/opa:opa:field:Args.Transport", - "internal/config/policy/opa:opa:field:Args.URL", - "internal/config/policy/opa:opa:func:Enabled", - "internal/config/policy/opa:opa:func:LookupConfig", - "internal/config/policy/opa:opa:func:New", - "internal/config/policy/opa:opa:func:SetPolicyOPAConfig", - "internal/config/policy/opa:opa:method:Args.UnmarshalJSON", - "internal/config/policy/opa:opa:method:Args.Validate", - "internal/config/policy/opa:opa:method:Opa.IsAllowed", - "internal/config/policy/opa:opa:type:Args", - "internal/config/policy/opa:opa:type:Opa", - "internal/config/policy/opa:opa:var:DefaultKVS", - "internal/config/policy/opa:opa:var:Help", - "internal/config/policy/plugin:plugin:const:AuthToken", - "internal/config/policy/plugin:plugin:const:EnableHTTP2", - "internal/config/policy/plugin:plugin:const:EnvPolicyPluginAuthToken", - "internal/config/policy/plugin:plugin:const:EnvPolicyPluginEnableHTTP2", - "internal/config/policy/plugin:plugin:const:EnvPolicyPluginURL", - "internal/config/policy/plugin:plugin:const:URL", - "internal/config/policy/plugin:plugin:field:Args.AuthToken", - "internal/config/policy/plugin:plugin:field:Args.CloseRespFn", - "internal/config/policy/plugin:plugin:field:Args.Transport", - "internal/config/policy/plugin:plugin:field:Args.URL", - "internal/config/policy/plugin:plugin:func:Enabled", - "internal/config/policy/plugin:plugin:func:LookupConfig", - "internal/config/policy/plugin:plugin:func:New", - "internal/config/policy/plugin:plugin:method:Args.UnmarshalJSON", - "internal/config/policy/plugin:plugin:method:Args.Validate", - "internal/config/policy/plugin:plugin:method:AuthZPlugin.IsAllowed", - "internal/config/policy/plugin:plugin:type:Args", - "internal/config/policy/plugin:plugin:type:AuthZPlugin", - "internal/config/policy/plugin:plugin:var:DefaultKVS", - "internal/config/policy/plugin:plugin:var:Help", - "internal/config/scanner:scanner:const:Cycle", - "internal/config/scanner:scanner:const:Delay", - "internal/config/scanner:scanner:const:EnvCycle", - "internal/config/scanner:scanner:const:EnvDelay", - "internal/config/scanner:scanner:const:EnvDelayLegacy", - "internal/config/scanner:scanner:const:EnvExcessFolders", - "internal/config/scanner:scanner:const:EnvExcessVersions", - "internal/config/scanner:scanner:const:EnvIdleSpeed", - "internal/config/scanner:scanner:const:EnvMaxWait", - "internal/config/scanner:scanner:const:EnvMaxWaitLegacy", - "internal/config/scanner:scanner:const:EnvSpeed", - "internal/config/scanner:scanner:const:ExcessFolders", - "internal/config/scanner:scanner:const:ExcessVersions", - "internal/config/scanner:scanner:const:IdleSpeed", - "internal/config/scanner:scanner:const:MaxWait", - "internal/config/scanner:scanner:const:Speed", - "internal/config/scanner:scanner:field:Config.Cycle", - "internal/config/scanner:scanner:field:Config.Delay", - "internal/config/scanner:scanner:field:Config.ExcessFolders", - "internal/config/scanner:scanner:field:Config.ExcessVersions", - "internal/config/scanner:scanner:field:Config.IdleMode", - "internal/config/scanner:scanner:field:Config.MaxWait", - "internal/config/scanner:scanner:func:LookupConfig", - "internal/config/scanner:scanner:type:Config", - "internal/config/scanner:scanner:var:DefaultKVS", - "internal/config/scanner:scanner:var:Help", - "internal/config/storageclass:storageclass:const:ClassRRS", - "internal/config/storageclass:storageclass:const:ClassStandard", - "internal/config/storageclass:storageclass:const:InlineBlock", - "internal/config/storageclass:storageclass:const:InlineBlockEnv", - "internal/config/storageclass:storageclass:const:Optimize", - "internal/config/storageclass:storageclass:const:OptimizeEnv", - "internal/config/storageclass:storageclass:const:RRS", - "internal/config/storageclass:storageclass:const:RRSEnv", - "internal/config/storageclass:storageclass:const:STANDARD", - "internal/config/storageclass:storageclass:const:StandardEnv", - "internal/config/storageclass:storageclass:field:Config.Optimize", - "internal/config/storageclass:storageclass:field:Config.RRS", - "internal/config/storageclass:storageclass:field:Config.Standard", - "internal/config/storageclass:storageclass:field:StorageClass.Parity", - "internal/config/storageclass:storageclass:func:DefaultParityBlocks", - "internal/config/storageclass:storageclass:func:Enabled", - "internal/config/storageclass:storageclass:func:IsValid", - "internal/config/storageclass:storageclass:func:LookupConfig", - "internal/config/storageclass:storageclass:func:SetStorageClass", - "internal/config/storageclass:storageclass:func:ValidateParity", - "internal/config/storageclass:storageclass:method:Config.AvailabilityOptimized", - "internal/config/storageclass:storageclass:method:Config.CapacityOptimized", - "internal/config/storageclass:storageclass:method:Config.GetParityForSC", - "internal/config/storageclass:storageclass:method:Config.InlineBlock", - "internal/config/storageclass:storageclass:method:Config.ShouldInline", - "internal/config/storageclass:storageclass:method:Config.UnmarshalJSON", - "internal/config/storageclass:storageclass:method:Config.Update", - "internal/config/storageclass:storageclass:method:StorageClass.MarshalText", - "internal/config/storageclass:storageclass:method:StorageClass.String", - "internal/config/storageclass:storageclass:method:StorageClass.UnmarshalText", - "internal/config/storageclass:storageclass:type:Config", - "internal/config/storageclass:storageclass:type:StorageClass", - "internal/config/storageclass:storageclass:var:ConfigLock", - "internal/config/storageclass:storageclass:var:DefaultKVS", - "internal/config/storageclass:storageclass:var:Help", - "internal/config/subnet:subnet:const:LoggerWebhookName", - "internal/config/subnet:subnet:field:Config.APIKey", - "internal/config/subnet:subnet:field:Config.BaseURL", - "internal/config/subnet:subnet:field:Config.License", - "internal/config/subnet:subnet:field:Config.Proxy", - "internal/config/subnet:subnet:func:LookupConfig", - "internal/config/subnet:subnet:method:Config.ApplyEnv", - "internal/config/subnet:subnet:method:Config.Post", - "internal/config/subnet:subnet:method:Config.Registered", - "internal/config/subnet:subnet:method:Config.Update", - "internal/config/subnet:subnet:method:Config.Upload", - "internal/config/subnet:subnet:type:Config", - "internal/config/subnet:subnet:var:DefaultKVS", - "internal/config/subnet:subnet:var:HelpSubnet", - "internal/config:config:const:APIKey", - "internal/config:config:const:APISubSys", - "internal/config:config:const:AccessKey", - "internal/config:config:const:AuditKafkaSubSys", - "internal/config:config:const:AuditWebhookSubSys", - "internal/config:config:const:BatchSubSys", - "internal/config:config:const:BrowserSubSys", - "internal/config:config:const:CallhomeSubSys", - "internal/config:config:const:Comment", - "internal/config:config:const:CompressionSubSys", - "internal/config:config:const:ContextKeyForTargetFromConfig", - "internal/config:config:const:CrawlerSubSys", - "internal/config:config:const:Default", - "internal/config:config:const:DefaultComment", - "internal/config:config:const:DriveSubSys", - "internal/config:config:const:Enable", - "internal/config:config:const:EnableOff", - "internal/config:config:const:EnableOn", - "internal/config:config:const:EnvAccessKey", - "internal/config:config:const:EnvAccessKeyFile", - "internal/config:config:const:EnvArgs", - "internal/config:config:const:EnvBrowser", - "internal/config:config:const:EnvBrowserLoginAnimation", - "internal/config:config:const:EnvBrowserRedirect", - "internal/config:config:const:EnvBrowserRedirectURL", - "internal/config:config:const:EnvBrowserSessionDuration", - "internal/config:config:const:EnvCertPassword", - "internal/config:config:const:EnvConfigEnvFile", - "internal/config:config:const:EnvConsoleDebugLogLevel", - "internal/config:config:const:EnvDNSWebhook", - "internal/config:config:const:EnvDomain", - "internal/config:config:const:EnvEndpoints", - "internal/config:config:const:EnvFSOSync", - "internal/config:config:const:EnvMinIOCallhomeEnable", - "internal/config:config:const:EnvMinIOCallhomeFrequency", - "internal/config:config:const:EnvMinIOLogQueryAuthToken", - "internal/config:config:const:EnvMinIOLogQueryURL", - "internal/config:config:const:EnvMinIOPrometheusAuthToken", - "internal/config:config:const:EnvMinIOPrometheusExtraLabels", - "internal/config:config:const:EnvMinIOPrometheusJobID", - "internal/config:config:const:EnvMinIOPrometheusURL", - "internal/config:config:const:EnvMinIOServerURL", - "internal/config:config:const:EnvMinIOSubnetAPIKey", - "internal/config:config:const:EnvMinIOSubnetLicense", - "internal/config:config:const:EnvMinIOSubnetProxy", - "internal/config:config:const:EnvMinioStsDuration", - "internal/config:config:const:EnvPrefix", - "internal/config:config:const:EnvPublicIPs", - "internal/config:config:const:EnvRegion", - "internal/config:config:const:EnvRegionName", - "internal/config:config:const:EnvRootDiskThresholdSize", - "internal/config:config:const:EnvRootDriveThresholdSize", - "internal/config:config:const:EnvRootPassword", - "internal/config:config:const:EnvRootPasswordFile", - "internal/config:config:const:EnvRootUser", - "internal/config:config:const:EnvRootUserFile", - "internal/config:config:const:EnvSecretKey", - "internal/config:config:const:EnvSecretKeyFile", - "internal/config:config:const:EnvSeparator", - "internal/config:config:const:EnvSiteName", - "internal/config:config:const:EnvSiteRegion", - "internal/config:config:const:EnvUpdate", - "internal/config:config:const:EnvVolumes", - "internal/config:config:const:EnvWordDelimiter", - "internal/config:config:const:EnvWorm", - "internal/config:config:const:EtcdSubSys", - "internal/config:config:const:HealSubSys", - "internal/config:config:const:ILMSubSys", - "internal/config:config:const:IdentityLDAPSubSys", - "internal/config:config:const:IdentityOpenIDSubSys", - "internal/config:config:const:IdentityPluginSubSys", - "internal/config:config:const:IdentityTLSSubSys", - "internal/config:config:const:KvComment", - "internal/config:config:const:KvDoubleQuote", - "internal/config:config:const:KvNewline", - "internal/config:config:const:KvSeparator", - "internal/config:config:const:KvSingleQuote", - "internal/config:config:const:KvSpaceSeparator", - "internal/config:config:const:LambdaWebhookSubSys", - "internal/config:config:const:License", - "internal/config:config:const:LoggerWebhookSubSys", - "internal/config:config:const:MaxExpiration", - "internal/config:config:const:MinExpiration", - "internal/config:config:const:NameKey", - "internal/config:config:const:NotifyAMQPSubSys", - "internal/config:config:const:NotifyESSubSys", - "internal/config:config:const:NotifyKafkaSubSys", - "internal/config:config:const:NotifyMQTTSubSys", - "internal/config:config:const:NotifyMySQLSubSys", - "internal/config:config:const:NotifyNATSSubSys", - "internal/config:config:const:NotifyNSQSubSys", - "internal/config:config:const:NotifyPostgresSubSys", - "internal/config:config:const:NotifyRedisSubSys", - "internal/config:config:const:NotifyWebhookSubSys", - "internal/config:config:const:PolicyOPASubSys", - "internal/config:config:const:PolicyPluginSubSys", - "internal/config:config:const:Proxy", - "internal/config:config:const:RegionKey", - "internal/config:config:const:RegionName", - "internal/config:config:const:RegionSubSys", - "internal/config:config:const:ScannerSubSys", - "internal/config:config:const:SecretKey", - "internal/config:config:const:SiteSubSys", - "internal/config:config:const:StorageClassSubSys", - "internal/config:config:const:SubSystemSeparator", - "internal/config:config:const:SubnetSubSys", - "internal/config:config:const:ValueSeparator", - "internal/config:config:const:ValueSourceAbsent", - "internal/config:config:const:ValueSourceCfg", - "internal/config:config:const:ValueSourceDef", - "internal/config:config:const:ValueSourceEnv", - "internal/config:config:field:EnvPair.Name", - "internal/config:config:field:EnvPair.Value", - "internal/config:config:field:HelpKV.Description", - "internal/config:config:field:HelpKV.Key", - "internal/config:config:field:HelpKV.MultipleTargets", - "internal/config:config:field:HelpKV.Optional", - "internal/config:config:field:HelpKV.Secret", - "internal/config:config:field:HelpKV.Sensitive", - "internal/config:config:field:HelpKV.Type", - "internal/config:config:field:KV.HiddenIfEmpty", - "internal/config:config:field:KV.Key", - "internal/config:config:field:KV.Value", - "internal/config:config:field:KVSrc.Key", - "internal/config:config:field:KVSrc.Src", - "internal/config:config:field:KVSrc.Value", - "internal/config:config:field:Opts.FTP", - "internal/config:config:field:Opts.SFTP", - "internal/config:config:field:ServerConfig.Pools", - "internal/config:config:field:ServerConfigCommon.Addr", - "internal/config:config:field:ServerConfigCommon.CertsDir", - "internal/config:config:field:ServerConfigCommon.ConsoleAddr", - "internal/config:config:field:ServerConfigCommon.Options", - "internal/config:config:field:ServerConfigCommon.RootPwd", - "internal/config:config:field:ServerConfigCommon.RootUser", - "internal/config:config:field:ServerConfigV1.Pools", - "internal/config:config:field:ServerConfigVersion.Version", - "internal/config:config:field:SubsysInfo.Config", - "internal/config:config:field:SubsysInfo.Defaults", - "internal/config:config:field:SubsysInfo.EnvMap", - "internal/config:config:field:SubsysInfo.SubSys", - "internal/config:config:field:SubsysInfo.Target", - "internal/config:config:field:Target.KVS", - "internal/config:config:field:Target.SubSystem", - "internal/config:config:func:CertificateText", - "internal/config:config:func:CheckValidKeys", - "internal/config:config:func:Decrypt", - "internal/config:config:func:DecryptBytes", - "internal/config:config:func:DefaultHelpPostfix", - "internal/config:config:func:Encrypt", - "internal/config:config:func:EncryptBytes", - "internal/config:config:func:EnsureCertAndKey", - "internal/config:config:func:Error", - "internal/config:config:func:ErrorToErr", - "internal/config:config:func:Errorf", - "internal/config:config:func:FmtError", - "internal/config:config:func:FormatBool", - "internal/config:config:func:GetSubSys", - "internal/config:config:func:LoadX509KeyPair", - "internal/config:config:func:LookupSite", - "internal/config:config:func:LookupWorm", - "internal/config:config:func:Merge", - "internal/config:config:func:New", - "internal/config:config:func:ParseBool", - "internal/config:config:func:ParseBoolFlag", - "internal/config:config:func:ParseConfigTargetID", - "internal/config:config:func:ParsePublicCertFile", - "internal/config:config:func:ParseTrustedProxies", - "internal/config:config:func:RegisterDefaultKVS", - "internal/config:config:func:RegisterHelpDeprecatedSubSys", - "internal/config:config:func:RegisterHelpSubSys", - "internal/config:config:func:SetRegion", - "internal/config:config:method:BoolFlag.MarshalJSON", - "internal/config:config:method:BoolFlag.String", - "internal/config:config:method:BoolFlag.UnmarshalJSON", - "internal/config:config:method:Config.CheckValidKeys", - "internal/config:config:method:Config.Clone", - "internal/config:config:method:Config.DelFrom", - "internal/config:config:method:Config.DelKVS", - "internal/config:config:method:Config.GetAvailableTargets", - "internal/config:config:method:Config.GetKVS", - "internal/config:config:method:Config.GetResolvedConfigParams", - "internal/config:config:method:Config.GetSubsysInfo", - "internal/config:config:method:Config.Merge", - "internal/config:config:method:Config.ReadConfig", - "internal/config:config:method:Config.RedactSensitiveInfo", - "internal/config:config:method:Config.ResolveConfigParam", - "internal/config:config:method:Config.SetKVS", - "internal/config:config:method:Err.Clone", - "internal/config:config:method:Err.Error", - "internal/config:config:method:Err.Hint", - "internal/config:config:method:Err.Msg", - "internal/config:config:method:Err.Msgf", - "internal/config:config:method:ErrConfigGeneric.Error", - "internal/config:config:method:HelpKVS.Lookup", - "internal/config:config:method:KV.String", - "internal/config:config:method:KVS.Clone", - "internal/config:config:method:KVS.Delete", - "internal/config:config:method:KVS.Empty", - "internal/config:config:method:KVS.Get", - "internal/config:config:method:KVS.GetWithDefault", - "internal/config:config:method:KVS.Keys", - "internal/config:config:method:KVS.Lookup", - "internal/config:config:method:KVS.LookupKV", - "internal/config:config:method:KVS.Set", - "internal/config:config:method:KVS.String", - "internal/config:config:method:Site.Name", - "internal/config:config:method:Site.Region", - "internal/config:config:method:Site.Update", - "internal/config:config:method:SubsysInfo.AddEnvString", - "internal/config:config:method:SubsysInfo.WriteTo", - "internal/config:config:method:TrustedProxies.Contains", - "internal/config:config:type:BoolFlag", - "internal/config:config:type:Config", - "internal/config:config:type:ContextKeyString", - "internal/config:config:type:EnvPair", - "internal/config:config:type:Err", - "internal/config:config:type:ErrConfigGeneric", - "internal/config:config:type:ErrConfigNotFound", - "internal/config:config:type:ErrFn", - "internal/config:config:type:ErrorConfig", - "internal/config:config:type:HelpKV", - "internal/config:config:type:HelpKVS", - "internal/config:config:type:KV", - "internal/config:config:type:KVS", - "internal/config:config:type:KVSrc", - "internal/config:config:type:Opts", - "internal/config:config:type:ServerConfig", - "internal/config:config:type:ServerConfigCommon", - "internal/config:config:type:ServerConfigV1", - "internal/config:config:type:ServerConfigVersion", - "internal/config:config:type:Site", - "internal/config:config:type:SubsysInfo", - "internal/config:config:type:Target", - "internal/config:config:type:Targets", - "internal/config:config:type:TrustedProxies", - "internal/config:config:type:ValueSource", - "internal/config:config:var:DefaultCredentialKVS", - "internal/config:config:var:DefaultKVS", - "internal/config:config:var:DefaultRegionKVS", - "internal/config:config:var:DefaultSiteKVS", - "internal/config:config:var:ErrCertsAndHTTPEndpoints", - "internal/config:config:var:ErrInvalidAddressFlag", - "internal/config:config:var:ErrInvalidBatchExpirationWorkersWait", - "internal/config:config:var:ErrInvalidBatchKeyRotationWorkersWait", - "internal/config:config:var:ErrInvalidBatchReplicationWorkersWait", - "internal/config:config:var:ErrInvalidBrowserValue", - "internal/config:config:var:ErrInvalidCompressionIncludesValue", - "internal/config:config:var:ErrInvalidConfigDecryptionKey", - "internal/config:config:var:ErrInvalidCredentials", - "internal/config:config:var:ErrInvalidDomainValue", - "internal/config:config:var:ErrInvalidEndpoint", - "internal/config:config:var:ErrInvalidErasureEndpoints", - "internal/config:config:var:ErrInvalidErasureSetSize", - "internal/config:config:var:ErrInvalidFSOSyncValue", - "internal/config:config:var:ErrInvalidNumberOfErasureEndpoints", - "internal/config:config:var:ErrInvalidReplicationWorkersValue", - "internal/config:config:var:ErrInvalidRootUserCredentials", - "internal/config:config:var:ErrInvalidTransitionWorkersValue", - "internal/config:config:var:ErrInvalidWormValue", - "internal/config:config:var:ErrInvalidXLValue", - "internal/config:config:var:ErrMissingEnvCredentialAccessKey", - "internal/config:config:var:ErrMissingEnvCredentialRootPassword", - "internal/config:config:var:ErrMissingEnvCredentialRootUser", - "internal/config:config:var:ErrMissingEnvCredentialSecretKey", - "internal/config:config:var:ErrNoCertsAndHTTPSEndpoints", - "internal/config:config:var:ErrOverlappingDomainValue", - "internal/config:config:var:ErrPortAccess", - "internal/config:config:var:ErrPortAlreadyInUse", - "internal/config:config:var:ErrStorageClassValue", - "internal/config:config:var:ErrTLSNoPassword", - "internal/config:config:var:ErrTLSReadError", - "internal/config:config:var:ErrTLSUnexpectedData", - "internal/config:config:var:ErrTLSWrongPassword", - "internal/config:config:var:ErrUnableToWriteInBackend", - "internal/config:config:var:ErrUnexpectedBackendVersion", - "internal/config:config:var:ErrUnexpectedError", - "internal/config:config:var:ErrUnsupportedBackend", - "internal/config:config:var:HelpDeprecatedSubSysMap", - "internal/config:config:var:HelpSubSysMap", - "internal/config:config:var:LambdaSubSystems", - "internal/config:config:var:LoggerSubSystems", - "internal/config:config:var:NotifySubSystems", - "internal/config:config:var:RegionHelp", - "internal/config:config:var:SiteHelp", - "internal/config:config:var:SubSystems", - "internal/config:config:var:SubSystemsDynamic", - "internal/config:config:var:SubSystemsSingleTargets", - "internal/crypto:crypto:const:ARNPrefix", - "internal/crypto:crypto:const:EnvKMSAutoEncryption", - "internal/crypto:crypto:const:InsecureSealAlgorithm", - "internal/crypto:crypto:const:MetaAlgorithm", - "internal/crypto:crypto:const:MetaContext", - "internal/crypto:crypto:const:MetaDataEncryptionKey", - "internal/crypto:crypto:const:MetaIV", - "internal/crypto:crypto:const:MetaKeyID", - "internal/crypto:crypto:const:MetaMultipart", - "internal/crypto:crypto:const:MetaSealedKeyKMS", - "internal/crypto:crypto:const:MetaSealedKeyS3", - "internal/crypto:crypto:const:MetaSealedKeySSEC", - "internal/crypto:crypto:const:MetaSsecCRC", - "internal/crypto:crypto:const:SealAlgorithm", - "internal/crypto:crypto:field:SealedKey.Algorithm", - "internal/crypto:crypto:field:SealedKey.IV", - "internal/crypto:crypto:field:SealedKey.Key", - "internal/crypto:crypto:field:Type.IsEncrypted", - "internal/crypto:crypto:field:Type.IsRequested", - "internal/crypto:crypto:func:CreateMultipartMetadata", - "internal/crypto:crypto:func:DARECiphers", - "internal/crypto:crypto:func:DecryptSinglePart", - "internal/crypto:crypto:func:EncryptMultiPart", - "internal/crypto:crypto:func:EncryptSinglePart", - "internal/crypto:crypto:func:Errorf", - "internal/crypto:crypto:func:GenerateIV", - "internal/crypto:crypto:func:GenerateKey", - "internal/crypto:crypto:func:IsETagSealed", - "internal/crypto:crypto:func:IsEncrypted", - "internal/crypto:crypto:func:IsMultiPart", - "internal/crypto:crypto:func:IsRequested", - "internal/crypto:crypto:func:IsSourceEncrypted", - "internal/crypto:crypto:func:LookupAutoEncryption", - "internal/crypto:crypto:func:RemoveInternalEntries", - "internal/crypto:crypto:func:RemoveSSEHeaders", - "internal/crypto:crypto:func:RemoveSensitiveEntries", - "internal/crypto:crypto:func:RemoveSensitiveHeaders", - "internal/crypto:crypto:func:Requested", - "internal/crypto:crypto:func:TLSCiphers", - "internal/crypto:crypto:func:TLSCiphersBackwardCompatible", - "internal/crypto:crypto:func:TLSCurveIDs", - "internal/crypto:crypto:method:Error.Error", - "internal/crypto:crypto:method:Error.Unwrap", - "internal/crypto:crypto:method:ObjectKey.DerivePartKey", - "internal/crypto:crypto:method:ObjectKey.Seal", - "internal/crypto:crypto:method:ObjectKey.SealETag", - "internal/crypto:crypto:method:ObjectKey.Unseal", - "internal/crypto:crypto:method:ObjectKey.UnsealETag", - "internal/crypto:crypto:method:ssec.CreateMetadata", - "internal/crypto:crypto:method:ssec.IsEncrypted", - "internal/crypto:crypto:method:ssec.IsRequested", - "internal/crypto:crypto:method:ssec.ParseHTTP", - "internal/crypto:crypto:method:ssec.ParseMetadata", - "internal/crypto:crypto:method:ssec.String", - "internal/crypto:crypto:method:ssec.UnsealObjectKey", - "internal/crypto:crypto:method:ssecCopy.IsRequested", - "internal/crypto:crypto:method:ssecCopy.ParseHTTP", - "internal/crypto:crypto:method:ssecCopy.UnsealObjectKey", - "internal/crypto:crypto:method:ssekms.CreateMetadata", - "internal/crypto:crypto:method:ssekms.IsEncrypted", - "internal/crypto:crypto:method:ssekms.IsRequested", - "internal/crypto:crypto:method:ssekms.ParseHTTP", - "internal/crypto:crypto:method:ssekms.ParseMetadata", - "internal/crypto:crypto:method:ssekms.String", - "internal/crypto:crypto:method:ssekms.UnsealObjectKey", - "internal/crypto:crypto:method:sses3.CreateMetadata", - "internal/crypto:crypto:method:sses3.IsEncrypted", - "internal/crypto:crypto:method:sses3.IsRequested", - "internal/crypto:crypto:method:sses3.ParseHTTP", - "internal/crypto:crypto:method:sses3.ParseMetadata", - "internal/crypto:crypto:method:sses3.String", - "internal/crypto:crypto:method:sses3.UnsealObjectKey", - "internal/crypto:crypto:method:sses3.UnsealObjectKeys", - "internal/crypto:crypto:type:Error", - "internal/crypto:crypto:type:ObjectKey", - "internal/crypto:crypto:type:SealedKey", - "internal/crypto:crypto:type:Type", - "internal/crypto:crypto:var:ErrCustomerKeyMD5Mismatch", - "internal/crypto:crypto:var:ErrIncompatibleEncryptionMethod", - "internal/crypto:crypto:var:ErrIncompatibleEncryptionWithCompression", - "internal/crypto:crypto:var:ErrInvalidCustomerAlgorithm", - "internal/crypto:crypto:var:ErrInvalidCustomerKey", - "internal/crypto:crypto:var:ErrInvalidEncryptionKeyID", - "internal/crypto:crypto:var:ErrInvalidEncryptionMethod", - "internal/crypto:crypto:var:ErrMissingCustomerKey", - "internal/crypto:crypto:var:ErrMissingCustomerKeyMD5", - "internal/crypto:crypto:var:ErrSecretKeyMismatch", - "internal/crypto:crypto:var:S3", - "internal/crypto:crypto:var:S3KMS", - "internal/crypto:crypto:var:SSEC", - "internal/crypto:crypto:var:SSECopy", - "internal/deadlineconn:deadlineconn:func:New", - "internal/deadlineconn:deadlineconn:func:Unwrap", - "internal/deadlineconn:deadlineconn:method:DeadlineConn.Close", - "internal/deadlineconn:deadlineconn:method:DeadlineConn.Read", - "internal/deadlineconn:deadlineconn:method:DeadlineConn.SetDeadline", - "internal/deadlineconn:deadlineconn:method:DeadlineConn.SetReadDeadline", - "internal/deadlineconn:deadlineconn:method:DeadlineConn.SetWriteDeadline", - "internal/deadlineconn:deadlineconn:method:DeadlineConn.WithReadDeadline", - "internal/deadlineconn:deadlineconn:method:DeadlineConn.WithWriteDeadline", - "internal/deadlineconn:deadlineconn:method:DeadlineConn.Write", - "internal/deadlineconn:deadlineconn:type:DeadlineConn", - "internal/disk:disk:const:ODirectPlatform", - "internal/disk:disk:field:IOStats.CurrentIOs", - "internal/disk:disk:field:IOStats.DiscardIOs", - "internal/disk:disk:field:IOStats.DiscardMerges", - "internal/disk:disk:field:IOStats.DiscardSectors", - "internal/disk:disk:field:IOStats.DiscardTicks", - "internal/disk:disk:field:IOStats.FlushIOs", - "internal/disk:disk:field:IOStats.FlushTicks", - "internal/disk:disk:field:IOStats.ReadIOs", - "internal/disk:disk:field:IOStats.ReadMerges", - "internal/disk:disk:field:IOStats.ReadSectors", - "internal/disk:disk:field:IOStats.ReadTicks", - "internal/disk:disk:field:IOStats.ReqTicks", - "internal/disk:disk:field:IOStats.TotalTicks", - "internal/disk:disk:field:IOStats.WriteIOs", - "internal/disk:disk:field:IOStats.WriteMerges", - "internal/disk:disk:field:IOStats.WriteSectors", - "internal/disk:disk:field:IOStats.WriteTicks", - "internal/disk:disk:field:Info.FSType", - "internal/disk:disk:field:Info.Ffree", - "internal/disk:disk:field:Info.Files", - "internal/disk:disk:field:Info.Free", - "internal/disk:disk:field:Info.Major", - "internal/disk:disk:field:Info.Minor", - "internal/disk:disk:field:Info.NRRequests", - "internal/disk:disk:field:Info.Name", - "internal/disk:disk:field:Info.Rotational", - "internal/disk:disk:field:Info.Total", - "internal/disk:disk:field:Info.Used", - "internal/disk:disk:func:AlignedBlock", - "internal/disk:disk:func:DisableDirectIO", - "internal/disk:disk:func:FadviseDontNeed", - "internal/disk:disk:func:Fdatasync", - "internal/disk:disk:func:GetDriveStats", - "internal/disk:disk:func:GetInfo", - "internal/disk:disk:func:IsRootDisk", - "internal/disk:disk:func:OpenFileDirectIO", - "internal/disk:disk:func:SameDisk", - "internal/disk:disk:type:IOStats", - "internal/disk:disk:type:Info", - "internal/disk:disk:var:GetDiskFreeSpace", - "internal/disk:disk:var:GetDiskFreeSpaceEx", - "internal/disk:disk:var:GetVolumeInformation", - "internal/dsync:dsync:const:RespErr", - "internal/dsync:dsync:const:RespLockConflict", - "internal/dsync:dsync:const:RespLockNotFound", - "internal/dsync:dsync:const:RespLockNotInitialized", - "internal/dsync:dsync:const:RespOK", - "internal/dsync:dsync:field:DRWMutex.Names", - "internal/dsync:dsync:field:Dsync.GetLockers", - "internal/dsync:dsync:field:Dsync.Timeouts", - "internal/dsync:dsync:field:LockArgs.Owner", - "internal/dsync:dsync:field:LockArgs.Quorum", - "internal/dsync:dsync:field:LockArgs.Resources", - "internal/dsync:dsync:field:LockArgs.Source", - "internal/dsync:dsync:field:LockArgs.UID", - "internal/dsync:dsync:field:LockResp.Code", - "internal/dsync:dsync:field:LockResp.Err", - "internal/dsync:dsync:field:NetLocker.Close", - "internal/dsync:dsync:field:NetLocker.ForceUnlock", - "internal/dsync:dsync:field:NetLocker.IsLocal", - "internal/dsync:dsync:field:NetLocker.IsOnline", - "internal/dsync:dsync:field:NetLocker.Lock", - "internal/dsync:dsync:field:NetLocker.RLock", - "internal/dsync:dsync:field:NetLocker.RUnlock", - "internal/dsync:dsync:field:NetLocker.Refresh", - "internal/dsync:dsync:field:NetLocker.String", - "internal/dsync:dsync:field:NetLocker.Unlock", - "internal/dsync:dsync:field:Options.RetryInterval", - "internal/dsync:dsync:field:Options.Timeout", - "internal/dsync:dsync:field:Timeouts.Acquire", - "internal/dsync:dsync:field:Timeouts.ForceUnlockCall", - "internal/dsync:dsync:field:Timeouts.RefreshCall", - "internal/dsync:dsync:field:Timeouts.UnlockCall", - "internal/dsync:dsync:func:NewDRWMutex", - "internal/dsync:dsync:method:DRWMutex.GetLock", - "internal/dsync:dsync:method:DRWMutex.GetRLock", - "internal/dsync:dsync:method:DRWMutex.Lock", - "internal/dsync:dsync:method:DRWMutex.RLock", - "internal/dsync:dsync:method:DRWMutex.RUnlock", - "internal/dsync:dsync:method:DRWMutex.Unlock", - "internal/dsync:dsync:method:LockArgs.DecodeMsg", - "internal/dsync:dsync:method:LockArgs.EncodeMsg", - "internal/dsync:dsync:method:LockArgs.MarshalMsg", - "internal/dsync:dsync:method:LockArgs.Msgsize", - "internal/dsync:dsync:method:LockArgs.UnmarshalMsg", - "internal/dsync:dsync:method:LockResp.DecodeMsg", - "internal/dsync:dsync:method:LockResp.EncodeMsg", - "internal/dsync:dsync:method:LockResp.MarshalMsg", - "internal/dsync:dsync:method:LockResp.Msgsize", - "internal/dsync:dsync:method:LockResp.UnmarshalMsg", - "internal/dsync:dsync:method:ResponseCode.DecodeMsg", - "internal/dsync:dsync:method:ResponseCode.EncodeMsg", - "internal/dsync:dsync:method:ResponseCode.MarshalMsg", - "internal/dsync:dsync:method:ResponseCode.Msgsize", - "internal/dsync:dsync:method:ResponseCode.UnmarshalMsg", - "internal/dsync:dsync:method:lockedRandSource.Int63", - "internal/dsync:dsync:method:lockedRandSource.Seed", - "internal/dsync:dsync:type:DRWMutex", - "internal/dsync:dsync:type:Dsync", - "internal/dsync:dsync:type:Granted", - "internal/dsync:dsync:type:LockArgs", - "internal/dsync:dsync:type:LockResp", - "internal/dsync:dsync:type:NetLocker", - "internal/dsync:dsync:type:Options", - "internal/dsync:dsync:type:ResponseCode", - "internal/dsync:dsync:type:Timeouts", - "internal/dsync:dsync:var:DefaultTimeouts", - "internal/etag:etag:field:Tagger.ETag", - "internal/etag:etag:field:VerifyError.Computed", - "internal/etag:etag:field:VerifyError.Expected", - "internal/etag:etag:func:ContentMD5Requested", - "internal/etag:etag:func:Decrypt", - "internal/etag:etag:func:Equal", - "internal/etag:etag:func:FromContentMD5", - "internal/etag:etag:func:Get", - "internal/etag:etag:func:Multipart", - "internal/etag:etag:func:NewReader", - "internal/etag:etag:func:NewUUIDHash", - "internal/etag:etag:func:Parse", - "internal/etag:etag:func:Set", - "internal/etag:etag:func:Wrap", - "internal/etag:etag:method:ETag.ETag", - "internal/etag:etag:method:ETag.Format", - "internal/etag:etag:method:ETag.IsEncrypted", - "internal/etag:etag:method:ETag.IsMultipart", - "internal/etag:etag:method:ETag.Parts", - "internal/etag:etag:method:ETag.String", - "internal/etag:etag:method:Reader.ETag", - "internal/etag:etag:method:Reader.Read", - "internal/etag:etag:method:UUIDHash.BlockSize", - "internal/etag:etag:method:UUIDHash.Reset", - "internal/etag:etag:method:UUIDHash.Size", - "internal/etag:etag:method:UUIDHash.Sum", - "internal/etag:etag:method:UUIDHash.Write", - "internal/etag:etag:method:VerifyError.Error", - "internal/etag:etag:method:wrapReader.ETag", - "internal/etag:etag:type:ETag", - "internal/etag:etag:type:Reader", - "internal/etag:etag:type:Tagger", - "internal/etag:etag:type:UUIDHash", - "internal/etag:etag:type:VerifyError", - "internal/event/target:target:const:AmqpArguments", - "internal/event/target:target:const:AmqpAutoDeleted", - "internal/event/target:target:const:AmqpDeliveryMode", - "internal/event/target:target:const:AmqpDurable", - "internal/event/target:target:const:AmqpExchange", - "internal/event/target:target:const:AmqpExchangeType", - "internal/event/target:target:const:AmqpImmediate", - "internal/event/target:target:const:AmqpInternal", - "internal/event/target:target:const:AmqpMandatory", - "internal/event/target:target:const:AmqpNoWait", - "internal/event/target:target:const:AmqpPublisherConfirms", - "internal/event/target:target:const:AmqpQueueDir", - "internal/event/target:target:const:AmqpQueueLimit", - "internal/event/target:target:const:AmqpRoutingKey", - "internal/event/target:target:const:AmqpURL", - "internal/event/target:target:const:ESSDeprecated", - "internal/event/target:target:const:ESSSupported", - "internal/event/target:target:const:ESSUnknown", - "internal/event/target:target:const:ESSUnsupported", - "internal/event/target:target:const:ElasticFormat", - "internal/event/target:target:const:ElasticIndex", - "internal/event/target:target:const:ElasticPassword", - "internal/event/target:target:const:ElasticQueueDir", - "internal/event/target:target:const:ElasticQueueLimit", - "internal/event/target:target:const:ElasticURL", - "internal/event/target:target:const:ElasticUsername", - "internal/event/target:target:const:EnvAMQPArguments", - "internal/event/target:target:const:EnvAMQPAutoDeleted", - "internal/event/target:target:const:EnvAMQPDeliveryMode", - "internal/event/target:target:const:EnvAMQPDurable", - "internal/event/target:target:const:EnvAMQPEnable", - "internal/event/target:target:const:EnvAMQPExchange", - "internal/event/target:target:const:EnvAMQPExchangeType", - "internal/event/target:target:const:EnvAMQPImmediate", - "internal/event/target:target:const:EnvAMQPInternal", - "internal/event/target:target:const:EnvAMQPMandatory", - "internal/event/target:target:const:EnvAMQPNoWait", - "internal/event/target:target:const:EnvAMQPPublisherConfirms", - "internal/event/target:target:const:EnvAMQPQueueDir", - "internal/event/target:target:const:EnvAMQPQueueLimit", - "internal/event/target:target:const:EnvAMQPRoutingKey", - "internal/event/target:target:const:EnvAMQPURL", - "internal/event/target:target:const:EnvElasticEnable", - "internal/event/target:target:const:EnvElasticFormat", - "internal/event/target:target:const:EnvElasticIndex", - "internal/event/target:target:const:EnvElasticPassword", - "internal/event/target:target:const:EnvElasticQueueDir", - "internal/event/target:target:const:EnvElasticQueueLimit", - "internal/event/target:target:const:EnvElasticURL", - "internal/event/target:target:const:EnvElasticUsername", - "internal/event/target:target:const:EnvKafkaBatchCommitTimeout", - "internal/event/target:target:const:EnvKafkaBatchSize", - "internal/event/target:target:const:EnvKafkaBrokers", - "internal/event/target:target:const:EnvKafkaClientTLSCert", - "internal/event/target:target:const:EnvKafkaClientTLSKey", - "internal/event/target:target:const:EnvKafkaEnable", - "internal/event/target:target:const:EnvKafkaProducerCompressionCodec", - "internal/event/target:target:const:EnvKafkaProducerCompressionLevel", - "internal/event/target:target:const:EnvKafkaQueueDir", - "internal/event/target:target:const:EnvKafkaQueueLimit", - "internal/event/target:target:const:EnvKafkaSASLEnable", - "internal/event/target:target:const:EnvKafkaSASLMechanism", - "internal/event/target:target:const:EnvKafkaSASLPassword", - "internal/event/target:target:const:EnvKafkaSASLUsername", - "internal/event/target:target:const:EnvKafkaTLS", - "internal/event/target:target:const:EnvKafkaTLSClientAuth", - "internal/event/target:target:const:EnvKafkaTLSSkipVerify", - "internal/event/target:target:const:EnvKafkaTopic", - "internal/event/target:target:const:EnvKafkaVersion", - "internal/event/target:target:const:EnvMQTTBroker", - "internal/event/target:target:const:EnvMQTTEnable", - "internal/event/target:target:const:EnvMQTTKeepAliveInterval", - "internal/event/target:target:const:EnvMQTTPassword", - "internal/event/target:target:const:EnvMQTTQoS", - "internal/event/target:target:const:EnvMQTTQueueDir", - "internal/event/target:target:const:EnvMQTTQueueLimit", - "internal/event/target:target:const:EnvMQTTReconnectInterval", - "internal/event/target:target:const:EnvMQTTTopic", - "internal/event/target:target:const:EnvMQTTUsername", - "internal/event/target:target:const:EnvMySQLDSNString", - "internal/event/target:target:const:EnvMySQLDatabase", - "internal/event/target:target:const:EnvMySQLEnable", - "internal/event/target:target:const:EnvMySQLFormat", - "internal/event/target:target:const:EnvMySQLHost", - "internal/event/target:target:const:EnvMySQLMaxOpenConnections", - "internal/event/target:target:const:EnvMySQLPassword", - "internal/event/target:target:const:EnvMySQLPort", - "internal/event/target:target:const:EnvMySQLQueueDir", - "internal/event/target:target:const:EnvMySQLQueueLimit", - "internal/event/target:target:const:EnvMySQLTable", - "internal/event/target:target:const:EnvMySQLUsername", - "internal/event/target:target:const:EnvNATSAddress", - "internal/event/target:target:const:EnvNATSCertAuthority", - "internal/event/target:target:const:EnvNATSClientCert", - "internal/event/target:target:const:EnvNATSClientKey", - "internal/event/target:target:const:EnvNATSEnable", - "internal/event/target:target:const:EnvNATSJetStream", - "internal/event/target:target:const:EnvNATSNKeySeed", - "internal/event/target:target:const:EnvNATSPassword", - "internal/event/target:target:const:EnvNATSPingInterval", - "internal/event/target:target:const:EnvNATSQueueDir", - "internal/event/target:target:const:EnvNATSQueueLimit", - "internal/event/target:target:const:EnvNATSStreaming", - "internal/event/target:target:const:EnvNATSStreamingAsync", - "internal/event/target:target:const:EnvNATSStreamingClusterID", - "internal/event/target:target:const:EnvNATSStreamingMaxPubAcksInFlight", - "internal/event/target:target:const:EnvNATSSubject", - "internal/event/target:target:const:EnvNATSTLS", - "internal/event/target:target:const:EnvNATSTLSSkipVerify", - "internal/event/target:target:const:EnvNATSToken", - "internal/event/target:target:const:EnvNATSUserCredentials", - "internal/event/target:target:const:EnvNATSUsername", - "internal/event/target:target:const:EnvNSQAddress", - "internal/event/target:target:const:EnvNSQEnable", - "internal/event/target:target:const:EnvNSQQueueDir", - "internal/event/target:target:const:EnvNSQQueueLimit", - "internal/event/target:target:const:EnvNSQTLS", - "internal/event/target:target:const:EnvNSQTLSSkipVerify", - "internal/event/target:target:const:EnvNSQTopic", - "internal/event/target:target:const:EnvNatsTLSHandshakeFirst", - "internal/event/target:target:const:EnvPostgresConnectionString", - "internal/event/target:target:const:EnvPostgresDatabase", - "internal/event/target:target:const:EnvPostgresEnable", - "internal/event/target:target:const:EnvPostgresFormat", - "internal/event/target:target:const:EnvPostgresHost", - "internal/event/target:target:const:EnvPostgresMaxOpenConnections", - "internal/event/target:target:const:EnvPostgresPassword", - "internal/event/target:target:const:EnvPostgresPort", - "internal/event/target:target:const:EnvPostgresQueueDir", - "internal/event/target:target:const:EnvPostgresQueueLimit", - "internal/event/target:target:const:EnvPostgresTable", - "internal/event/target:target:const:EnvPostgresUsername", - "internal/event/target:target:const:EnvRedisAddress", - "internal/event/target:target:const:EnvRedisEnable", - "internal/event/target:target:const:EnvRedisFormat", - "internal/event/target:target:const:EnvRedisKey", - "internal/event/target:target:const:EnvRedisPassword", - "internal/event/target:target:const:EnvRedisQueueDir", - "internal/event/target:target:const:EnvRedisQueueLimit", - "internal/event/target:target:const:EnvRedisUser", - "internal/event/target:target:const:EnvWebhookAuthToken", - "internal/event/target:target:const:EnvWebhookClientCert", - "internal/event/target:target:const:EnvWebhookClientKey", - "internal/event/target:target:const:EnvWebhookEnable", - "internal/event/target:target:const:EnvWebhookEndpoint", - "internal/event/target:target:const:EnvWebhookQueueDir", - "internal/event/target:target:const:EnvWebhookQueueLimit", - "internal/event/target:target:const:KafkaBatchCommitTimeout", - "internal/event/target:target:const:KafkaBatchSize", - "internal/event/target:target:const:KafkaBrokers", - "internal/event/target:target:const:KafkaClientTLSCert", - "internal/event/target:target:const:KafkaClientTLSKey", - "internal/event/target:target:const:KafkaCompressionCodec", - "internal/event/target:target:const:KafkaCompressionLevel", - "internal/event/target:target:const:KafkaQueueDir", - "internal/event/target:target:const:KafkaQueueLimit", - "internal/event/target:target:const:KafkaSASL", - "internal/event/target:target:const:KafkaSASLMechanism", - "internal/event/target:target:const:KafkaSASLPassword", - "internal/event/target:target:const:KafkaSASLUsername", - "internal/event/target:target:const:KafkaTLS", - "internal/event/target:target:const:KafkaTLSClientAuth", - "internal/event/target:target:const:KafkaTLSSkipVerify", - "internal/event/target:target:const:KafkaTopic", - "internal/event/target:target:const:KafkaVersion", - "internal/event/target:target:const:MqttBroker", - "internal/event/target:target:const:MqttKeepAliveInterval", - "internal/event/target:target:const:MqttPassword", - "internal/event/target:target:const:MqttQoS", - "internal/event/target:target:const:MqttQueueDir", - "internal/event/target:target:const:MqttQueueLimit", - "internal/event/target:target:const:MqttReconnectInterval", - "internal/event/target:target:const:MqttTopic", - "internal/event/target:target:const:MqttUsername", - "internal/event/target:target:const:MySQLDSNString", - "internal/event/target:target:const:MySQLDatabase", - "internal/event/target:target:const:MySQLFormat", - "internal/event/target:target:const:MySQLHost", - "internal/event/target:target:const:MySQLMaxOpenConnections", - "internal/event/target:target:const:MySQLPassword", - "internal/event/target:target:const:MySQLPort", - "internal/event/target:target:const:MySQLQueueDir", - "internal/event/target:target:const:MySQLQueueLimit", - "internal/event/target:target:const:MySQLTable", - "internal/event/target:target:const:MySQLUsername", - "internal/event/target:target:const:NATSAddress", - "internal/event/target:target:const:NATSCertAuthority", - "internal/event/target:target:const:NATSClientCert", - "internal/event/target:target:const:NATSClientKey", - "internal/event/target:target:const:NATSJetStream", - "internal/event/target:target:const:NATSNKeySeed", - "internal/event/target:target:const:NATSPassword", - "internal/event/target:target:const:NATSPingInterval", - "internal/event/target:target:const:NATSQueueDir", - "internal/event/target:target:const:NATSQueueLimit", - "internal/event/target:target:const:NATSStreaming", - "internal/event/target:target:const:NATSStreamingAsync", - "internal/event/target:target:const:NATSStreamingClusterID", - "internal/event/target:target:const:NATSStreamingMaxPubAcksInFlight", - "internal/event/target:target:const:NATSSubject", - "internal/event/target:target:const:NATSTLS", - "internal/event/target:target:const:NATSTLSHandshakeFirst", - "internal/event/target:target:const:NATSTLSSkipVerify", - "internal/event/target:target:const:NATSToken", - "internal/event/target:target:const:NATSUserCredentials", - "internal/event/target:target:const:NATSUsername", - "internal/event/target:target:const:NSQAddress", - "internal/event/target:target:const:NSQQueueDir", - "internal/event/target:target:const:NSQQueueLimit", - "internal/event/target:target:const:NSQTLS", - "internal/event/target:target:const:NSQTLSSkipVerify", - "internal/event/target:target:const:NSQTopic", - "internal/event/target:target:const:PostgresConnectionString", - "internal/event/target:target:const:PostgresDatabase", - "internal/event/target:target:const:PostgresFormat", - "internal/event/target:target:const:PostgresHost", - "internal/event/target:target:const:PostgresMaxOpenConnections", - "internal/event/target:target:const:PostgresPassword", - "internal/event/target:target:const:PostgresPort", - "internal/event/target:target:const:PostgresQueueDir", - "internal/event/target:target:const:PostgresQueueLimit", - "internal/event/target:target:const:PostgresTable", - "internal/event/target:target:const:PostgresUsername", - "internal/event/target:target:const:RedisAddress", - "internal/event/target:target:const:RedisFormat", - "internal/event/target:target:const:RedisKey", - "internal/event/target:target:const:RedisPassword", - "internal/event/target:target:const:RedisQueueDir", - "internal/event/target:target:const:RedisQueueLimit", - "internal/event/target:target:const:RedisUser", - "internal/event/target:target:const:WebhookAuthToken", - "internal/event/target:target:const:WebhookClientCert", - "internal/event/target:target:const:WebhookClientKey", - "internal/event/target:target:const:WebhookEndpoint", - "internal/event/target:target:const:WebhookQueueDir", - "internal/event/target:target:const:WebhookQueueLimit", - "internal/event/target:target:field:AMQPArgs.AutoDeleted", - "internal/event/target:target:field:AMQPArgs.DeliveryMode", - "internal/event/target:target:field:AMQPArgs.Durable", - "internal/event/target:target:field:AMQPArgs.Enable", - "internal/event/target:target:field:AMQPArgs.Exchange", - "internal/event/target:target:field:AMQPArgs.ExchangeType", - "internal/event/target:target:field:AMQPArgs.Immediate", - "internal/event/target:target:field:AMQPArgs.Internal", - "internal/event/target:target:field:AMQPArgs.Mandatory", - "internal/event/target:target:field:AMQPArgs.NoWait", - "internal/event/target:target:field:AMQPArgs.PublisherConfirms", - "internal/event/target:target:field:AMQPArgs.QueueDir", - "internal/event/target:target:field:AMQPArgs.QueueLimit", - "internal/event/target:target:field:AMQPArgs.RoutingKey", - "internal/event/target:target:field:AMQPArgs.URL", - "internal/event/target:target:field:ElasticsearchArgs.Enable", - "internal/event/target:target:field:ElasticsearchArgs.Format", - "internal/event/target:target:field:ElasticsearchArgs.Index", - "internal/event/target:target:field:ElasticsearchArgs.Password", - "internal/event/target:target:field:ElasticsearchArgs.QueueDir", - "internal/event/target:target:field:ElasticsearchArgs.QueueLimit", - "internal/event/target:target:field:ElasticsearchArgs.Transport", - "internal/event/target:target:field:ElasticsearchArgs.URL", - "internal/event/target:target:field:ElasticsearchArgs.Username", - "internal/event/target:target:field:KafkaArgs.BatchCommitTimeout", - "internal/event/target:target:field:KafkaArgs.BatchSize", - "internal/event/target:target:field:KafkaArgs.Brokers", - "internal/event/target:target:field:KafkaArgs.Enable", - "internal/event/target:target:field:KafkaArgs.Producer", - "internal/event/target:target:field:KafkaArgs.QueueDir", - "internal/event/target:target:field:KafkaArgs.QueueLimit", - "internal/event/target:target:field:KafkaArgs.SASL", - "internal/event/target:target:field:KafkaArgs.TLS", - "internal/event/target:target:field:KafkaArgs.Topic", - "internal/event/target:target:field:KafkaArgs.Version", - "internal/event/target:target:field:MQTTArgs.Broker", - "internal/event/target:target:field:MQTTArgs.Enable", - "internal/event/target:target:field:MQTTArgs.KeepAlive", - "internal/event/target:target:field:MQTTArgs.MaxReconnectInterval", - "internal/event/target:target:field:MQTTArgs.Password", - "internal/event/target:target:field:MQTTArgs.QoS", - "internal/event/target:target:field:MQTTArgs.QueueDir", - "internal/event/target:target:field:MQTTArgs.QueueLimit", - "internal/event/target:target:field:MQTTArgs.RootCAs", - "internal/event/target:target:field:MQTTArgs.Topic", - "internal/event/target:target:field:MQTTArgs.User", - "internal/event/target:target:field:MySQLArgs.DSN", - "internal/event/target:target:field:MySQLArgs.Database", - "internal/event/target:target:field:MySQLArgs.Enable", - "internal/event/target:target:field:MySQLArgs.Format", - "internal/event/target:target:field:MySQLArgs.Host", - "internal/event/target:target:field:MySQLArgs.MaxOpenConnections", - "internal/event/target:target:field:MySQLArgs.Password", - "internal/event/target:target:field:MySQLArgs.Port", - "internal/event/target:target:field:MySQLArgs.QueueDir", - "internal/event/target:target:field:MySQLArgs.QueueLimit", - "internal/event/target:target:field:MySQLArgs.Table", - "internal/event/target:target:field:MySQLArgs.User", - "internal/event/target:target:field:NATSArgs.Address", - "internal/event/target:target:field:NATSArgs.CertAuthority", - "internal/event/target:target:field:NATSArgs.ClientCert", - "internal/event/target:target:field:NATSArgs.ClientKey", - "internal/event/target:target:field:NATSArgs.Enable", - "internal/event/target:target:field:NATSArgs.JetStream", - "internal/event/target:target:field:NATSArgs.NKeySeed", - "internal/event/target:target:field:NATSArgs.Password", - "internal/event/target:target:field:NATSArgs.PingInterval", - "internal/event/target:target:field:NATSArgs.QueueDir", - "internal/event/target:target:field:NATSArgs.QueueLimit", - "internal/event/target:target:field:NATSArgs.RootCAs", - "internal/event/target:target:field:NATSArgs.Secure", - "internal/event/target:target:field:NATSArgs.Streaming", - "internal/event/target:target:field:NATSArgs.Subject", - "internal/event/target:target:field:NATSArgs.TLS", - "internal/event/target:target:field:NATSArgs.TLSHandshakeFirst", - "internal/event/target:target:field:NATSArgs.TLSSkipVerify", - "internal/event/target:target:field:NATSArgs.Token", - "internal/event/target:target:field:NATSArgs.UserCredentials", - "internal/event/target:target:field:NATSArgs.Username", - "internal/event/target:target:field:NSQArgs.Enable", - "internal/event/target:target:field:NSQArgs.NSQDAddress", - "internal/event/target:target:field:NSQArgs.QueueDir", - "internal/event/target:target:field:NSQArgs.QueueLimit", - "internal/event/target:target:field:NSQArgs.TLS", - "internal/event/target:target:field:NSQArgs.Topic", - "internal/event/target:target:field:PostgreSQLArgs.ConnectionString", - "internal/event/target:target:field:PostgreSQLArgs.Database", - "internal/event/target:target:field:PostgreSQLArgs.Enable", - "internal/event/target:target:field:PostgreSQLArgs.Format", - "internal/event/target:target:field:PostgreSQLArgs.Host", - "internal/event/target:target:field:PostgreSQLArgs.MaxOpenConnections", - "internal/event/target:target:field:PostgreSQLArgs.Password", - "internal/event/target:target:field:PostgreSQLArgs.Port", - "internal/event/target:target:field:PostgreSQLArgs.QueueDir", - "internal/event/target:target:field:PostgreSQLArgs.QueueLimit", - "internal/event/target:target:field:PostgreSQLArgs.Table", - "internal/event/target:target:field:PostgreSQLArgs.Username", - "internal/event/target:target:field:RedisAccessEvent.Event", - "internal/event/target:target:field:RedisAccessEvent.EventTime", - "internal/event/target:target:field:RedisArgs.Addr", - "internal/event/target:target:field:RedisArgs.Enable", - "internal/event/target:target:field:RedisArgs.Format", - "internal/event/target:target:field:RedisArgs.Key", - "internal/event/target:target:field:RedisArgs.Password", - "internal/event/target:target:field:RedisArgs.QueueDir", - "internal/event/target:target:field:RedisArgs.QueueLimit", - "internal/event/target:target:field:RedisArgs.User", - "internal/event/target:target:field:WebhookArgs.AuthToken", - "internal/event/target:target:field:WebhookArgs.ClientCert", - "internal/event/target:target:field:WebhookArgs.ClientKey", - "internal/event/target:target:field:WebhookArgs.Enable", - "internal/event/target:target:field:WebhookArgs.Endpoint", - "internal/event/target:target:field:WebhookArgs.QueueDir", - "internal/event/target:target:field:WebhookArgs.QueueLimit", - "internal/event/target:target:field:WebhookArgs.Transport", - "internal/event/target:target:func:IsConnErr", - "internal/event/target:target:func:NewAMQPTarget", - "internal/event/target:target:func:NewElasticsearchTarget", - "internal/event/target:target:func:NewKafkaTarget", - "internal/event/target:target:func:NewMQTTTarget", - "internal/event/target:target:func:NewMySQLTarget", - "internal/event/target:target:func:NewNATSTarget", - "internal/event/target:target:func:NewNSQTarget", - "internal/event/target:target:func:NewPostgreSQLTarget", - "internal/event/target:target:func:NewRedisTarget", - "internal/event/target:target:func:NewWebhookTarget", - "internal/event/target:target:method:AMQPArgs.Validate", - "internal/event/target:target:method:AMQPTarget.Close", - "internal/event/target:target:method:AMQPTarget.ID", - "internal/event/target:target:method:AMQPTarget.IsActive", - "internal/event/target:target:method:AMQPTarget.Name", - "internal/event/target:target:method:AMQPTarget.Save", - "internal/event/target:target:method:AMQPTarget.SendFromStore", - "internal/event/target:target:method:AMQPTarget.Store", - "internal/event/target:target:method:ElasticsearchArgs.Validate", - "internal/event/target:target:method:ElasticsearchTarget.Close", - "internal/event/target:target:method:ElasticsearchTarget.ID", - "internal/event/target:target:method:ElasticsearchTarget.IsActive", - "internal/event/target:target:method:ElasticsearchTarget.Name", - "internal/event/target:target:method:ElasticsearchTarget.Save", - "internal/event/target:target:method:ElasticsearchTarget.SendFromStore", - "internal/event/target:target:method:ElasticsearchTarget.Store", - "internal/event/target:target:method:KafkaArgs.Validate", - "internal/event/target:target:method:KafkaTarget.Close", - "internal/event/target:target:method:KafkaTarget.ID", - "internal/event/target:target:method:KafkaTarget.IsActive", - "internal/event/target:target:method:KafkaTarget.Name", - "internal/event/target:target:method:KafkaTarget.Save", - "internal/event/target:target:method:KafkaTarget.SendFromStore", - "internal/event/target:target:method:KafkaTarget.Store", - "internal/event/target:target:method:MQTTArgs.Validate", - "internal/event/target:target:method:MQTTTarget.Close", - "internal/event/target:target:method:MQTTTarget.ID", - "internal/event/target:target:method:MQTTTarget.IsActive", - "internal/event/target:target:method:MQTTTarget.Name", - "internal/event/target:target:method:MQTTTarget.Save", - "internal/event/target:target:method:MQTTTarget.SendFromStore", - "internal/event/target:target:method:MQTTTarget.Store", - "internal/event/target:target:method:MySQLArgs.Validate", - "internal/event/target:target:method:MySQLTarget.Close", - "internal/event/target:target:method:MySQLTarget.ID", - "internal/event/target:target:method:MySQLTarget.IsActive", - "internal/event/target:target:method:MySQLTarget.Name", - "internal/event/target:target:method:MySQLTarget.Save", - "internal/event/target:target:method:MySQLTarget.SendFromStore", - "internal/event/target:target:method:MySQLTarget.Store", - "internal/event/target:target:method:NATSArgs.Validate", - "internal/event/target:target:method:NATSTarget.Close", - "internal/event/target:target:method:NATSTarget.ID", - "internal/event/target:target:method:NATSTarget.IsActive", - "internal/event/target:target:method:NATSTarget.Name", - "internal/event/target:target:method:NATSTarget.Save", - "internal/event/target:target:method:NATSTarget.SendFromStore", - "internal/event/target:target:method:NATSTarget.Store", - "internal/event/target:target:method:NSQArgs.Validate", - "internal/event/target:target:method:NSQTarget.Close", - "internal/event/target:target:method:NSQTarget.ID", - "internal/event/target:target:method:NSQTarget.IsActive", - "internal/event/target:target:method:NSQTarget.Name", - "internal/event/target:target:method:NSQTarget.Save", - "internal/event/target:target:method:NSQTarget.SendFromStore", - "internal/event/target:target:method:NSQTarget.Store", - "internal/event/target:target:method:PostgreSQLArgs.Validate", - "internal/event/target:target:method:PostgreSQLTarget.Close", - "internal/event/target:target:method:PostgreSQLTarget.ID", - "internal/event/target:target:method:PostgreSQLTarget.IsActive", - "internal/event/target:target:method:PostgreSQLTarget.Name", - "internal/event/target:target:method:PostgreSQLTarget.Save", - "internal/event/target:target:method:PostgreSQLTarget.SendFromStore", - "internal/event/target:target:method:PostgreSQLTarget.Store", - "internal/event/target:target:method:RedisArgs.Validate", - "internal/event/target:target:method:RedisTarget.Close", - "internal/event/target:target:method:RedisTarget.ID", - "internal/event/target:target:method:RedisTarget.IsActive", - "internal/event/target:target:method:RedisTarget.Name", - "internal/event/target:target:method:RedisTarget.Save", - "internal/event/target:target:method:RedisTarget.SendFromStore", - "internal/event/target:target:method:RedisTarget.Store", - "internal/event/target:target:method:WebhookArgs.Validate", - "internal/event/target:target:method:WebhookTarget.Close", - "internal/event/target:target:method:WebhookTarget.ID", - "internal/event/target:target:method:WebhookTarget.IsActive", - "internal/event/target:target:method:WebhookTarget.Name", - "internal/event/target:target:method:WebhookTarget.Save", - "internal/event/target:target:method:WebhookTarget.SendFromStore", - "internal/event/target:target:method:WebhookTarget.Store", - "internal/event/target:target:method:XDGSCRAMClient.Begin", - "internal/event/target:target:method:XDGSCRAMClient.Done", - "internal/event/target:target:method:XDGSCRAMClient.Step", - "internal/event/target:target:type:AMQPArgs", - "internal/event/target:target:type:AMQPTarget", - "internal/event/target:target:type:ESSupportStatus", - "internal/event/target:target:type:ElasticsearchArgs", - "internal/event/target:target:type:ElasticsearchTarget", - "internal/event/target:target:type:KafkaArgs", - "internal/event/target:target:type:KafkaTarget", - "internal/event/target:target:type:MQTTArgs", - "internal/event/target:target:type:MQTTTarget", - "internal/event/target:target:type:MySQLArgs", - "internal/event/target:target:type:MySQLTarget", - "internal/event/target:target:type:NATSArgs", - "internal/event/target:target:type:NATSTarget", - "internal/event/target:target:type:NSQArgs", - "internal/event/target:target:type:NSQTarget", - "internal/event/target:target:type:PostgreSQLArgs", - "internal/event/target:target:type:PostgreSQLTarget", - "internal/event/target:target:type:RedisAccessEvent", - "internal/event/target:target:type:RedisArgs", - "internal/event/target:target:type:RedisTarget", - "internal/event/target:target:type:WebhookArgs", - "internal/event/target:target:type:WebhookTarget", - "internal/event/target:target:type:XDGSCRAMClient", - "internal/event/target:target:var:KafkaSHA256", - "internal/event/target:target:var:KafkaSHA512", - "internal/event:event:const:AMZTimeFormat", - "internal/event:event:const:AccessFormat", - "internal/event:event:const:BucketCreated", - "internal/event:event:const:BucketRemoved", - "internal/event:event:const:Everything", - "internal/event:event:const:ILMDelMarkerExpirationDelete", - "internal/event:event:const:NamespaceFormat", - "internal/event:event:const:ObjectAccessedAll", - "internal/event:event:const:ObjectAccessedAttributes", - "internal/event:event:const:ObjectAccessedGet", - "internal/event:event:const:ObjectAccessedGetLegalHold", - "internal/event:event:const:ObjectAccessedGetRetention", - "internal/event:event:const:ObjectAccessedHead", - "internal/event:event:const:ObjectCreatedAll", - "internal/event:event:const:ObjectCreatedCompleteMultipartUpload", - "internal/event:event:const:ObjectCreatedCopy", - "internal/event:event:const:ObjectCreatedDeleteTagging", - "internal/event:event:const:ObjectCreatedPost", - "internal/event:event:const:ObjectCreatedPut", - "internal/event:event:const:ObjectCreatedPutLegalHold", - "internal/event:event:const:ObjectCreatedPutRetention", - "internal/event:event:const:ObjectCreatedPutTagging", - "internal/event:event:const:ObjectLargeVersions", - "internal/event:event:const:ObjectManyVersions", - "internal/event:event:const:ObjectRemovedAll", - "internal/event:event:const:ObjectRemovedDelete", - "internal/event:event:const:ObjectRemovedDeleteAllVersions", - "internal/event:event:const:ObjectRemovedDeleteMarkerCreated", - "internal/event:event:const:ObjectRemovedNoOP", - "internal/event:event:const:ObjectReplicationAll", - "internal/event:event:const:ObjectReplicationComplete", - "internal/event:event:const:ObjectReplicationFailed", - "internal/event:event:const:ObjectReplicationMissedThreshold", - "internal/event:event:const:ObjectReplicationNotTracked", - "internal/event:event:const:ObjectReplicationReplicatedAfterThreshold", - "internal/event:event:const:ObjectRestoreAll", - "internal/event:event:const:ObjectRestoreCompleted", - "internal/event:event:const:ObjectRestorePost", - "internal/event:event:const:ObjectScannerAll", - "internal/event:event:const:ObjectTransitionAll", - "internal/event:event:const:ObjectTransitionComplete", - "internal/event:event:const:ObjectTransitionFailed", - "internal/event:event:const:PrefixManyFolders", - "internal/event:event:const:StoreExtension", - "internal/event:event:field:Bucket.ARN", - "internal/event:event:field:Bucket.Name", - "internal/event:event:field:Bucket.OwnerIdentity", - "internal/event:event:field:Config.LambdaList", - "internal/event:event:field:Config.QueueList", - "internal/event:event:field:Config.TopicList", - "internal/event:event:field:Config.XMLNS", - "internal/event:event:field:Config.XMLName", - "internal/event:event:field:ErrARNNotFound.ARN", - "internal/event:event:field:ErrDuplicateEventName.EventName", - "internal/event:event:field:ErrDuplicateQueueConfiguration.Queue", - "internal/event:event:field:ErrInvalidARN.ARN", - "internal/event:event:field:ErrInvalidEventName.Name", - "internal/event:event:field:ErrInvalidFilterName.FilterName", - "internal/event:event:field:ErrInvalidFilterValue.FilterValue", - "internal/event:event:field:ErrUnknownRegion.Region", - "internal/event:event:field:Event.AwsRegion", - "internal/event:event:field:Event.EventName", - "internal/event:event:field:Event.EventSource", - "internal/event:event:field:Event.EventTime", - "internal/event:event:field:Event.EventVersion", - "internal/event:event:field:Event.RequestParameters", - "internal/event:event:field:Event.ResponseElements", - "internal/event:event:field:Event.S3", - "internal/event:event:field:Event.Source", - "internal/event:event:field:Event.Type", - "internal/event:event:field:Event.UserIdentity", - "internal/event:event:field:FilterRule.Name", - "internal/event:event:field:FilterRule.Value", - "internal/event:event:field:FilterRuleList.Rules", - "internal/event:event:field:Identity.PrincipalID", - "internal/event:event:field:Log.EventName", - "internal/event:event:field:Log.Key", - "internal/event:event:field:Log.Records", - "internal/event:event:field:Metadata.Bucket", - "internal/event:event:field:Metadata.ConfigurationID", - "internal/event:event:field:Metadata.Object", - "internal/event:event:field:Metadata.SchemaVersion", - "internal/event:event:field:Object.ContentType", - "internal/event:event:field:Object.ETag", - "internal/event:event:field:Object.Key", - "internal/event:event:field:Object.Sequencer", - "internal/event:event:field:Object.Size", - "internal/event:event:field:Object.UserMetadata", - "internal/event:event:field:Object.VersionID", - "internal/event:event:field:Queue.ARN", - "internal/event:event:field:S3Key.RuleList", - "internal/event:event:field:Source.Host", - "internal/event:event:field:Source.Port", - "internal/event:event:field:Source.UserAgent", - "internal/event:event:field:Stats.CurrentQueuedCalls", - "internal/event:event:field:Stats.CurrentSendCalls", - "internal/event:event:field:Stats.EventsErrorsTotal", - "internal/event:event:field:Stats.EventsSkipped", - "internal/event:event:field:Stats.TargetStats", - "internal/event:event:field:Stats.TotalEvents", - "internal/event:event:field:Target.Close", - "internal/event:event:field:Target.ID", - "internal/event:event:field:Target.IsActive", - "internal/event:event:field:Target.Save", - "internal/event:event:field:Target.SendFromStore", - "internal/event:event:field:Target.Store", - "internal/event:event:field:TargetID.ID", - "internal/event:event:field:TargetID.Name", - "internal/event:event:field:TargetIDResult.Err", - "internal/event:event:field:TargetIDResult.ID", - "internal/event:event:field:TargetStat.CurrentQueue", - "internal/event:event:field:TargetStat.CurrentSendCalls", - "internal/event:event:field:TargetStat.FailedEvents", - "internal/event:event:field:TargetStat.TotalEvents", - "internal/event:event:field:TargetStore.Len", - "internal/event:event:func:IsEventError", - "internal/event:event:func:NewPattern", - "internal/event:event:func:NewRulesMap", - "internal/event:event:func:NewTargetIDSet", - "internal/event:event:func:NewTargetList", - "internal/event:event:func:ParseConfig", - "internal/event:event:func:ParseName", - "internal/event:event:func:ValidateFilterRuleValue", - "internal/event:event:method:ARN.MarshalXML", - "internal/event:event:method:ARN.String", - "internal/event:event:method:ARN.UnmarshalXML", - "internal/event:event:method:Config.SetRegion", - "internal/event:event:method:Config.ToRulesMap", - "internal/event:event:method:Config.UnmarshalXML", - "internal/event:event:method:Config.Validate", - "internal/event:event:method:ErrARNNotFound.Error", - "internal/event:event:method:ErrDuplicateEventName.Error", - "internal/event:event:method:ErrDuplicateQueueConfiguration.Error", - "internal/event:event:method:ErrFilterNamePrefix.Error", - "internal/event:event:method:ErrFilterNameSuffix.Error", - "internal/event:event:method:ErrInvalidARN.Error", - "internal/event:event:method:ErrInvalidEventName.Error", - "internal/event:event:method:ErrInvalidFilterName.Error", - "internal/event:event:method:ErrInvalidFilterValue.Error", - "internal/event:event:method:ErrUnknownRegion.Error", - "internal/event:event:method:ErrUnsupportedConfiguration.Error", - "internal/event:event:method:Event.Mask", - "internal/event:event:method:FilterRule.MarshalXML", - "internal/event:event:method:FilterRule.UnmarshalXML", - "internal/event:event:method:FilterRuleList.Pattern", - "internal/event:event:method:FilterRuleList.UnmarshalXML", - "internal/event:event:method:Name.Expand", - "internal/event:event:method:Name.MarshalJSON", - "internal/event:event:method:Name.MarshalXML", - "internal/event:event:method:Name.Mask", - "internal/event:event:method:Name.String", - "internal/event:event:method:Name.UnmarshalJSON", - "internal/event:event:method:Name.UnmarshalXML", - "internal/event:event:method:Queue.SetRegion", - "internal/event:event:method:Queue.ToRulesMap", - "internal/event:event:method:Queue.UnmarshalXML", - "internal/event:event:method:Queue.Validate", - "internal/event:event:method:Rules.Add", - "internal/event:event:method:Rules.Clone", - "internal/event:event:method:Rules.Difference", - "internal/event:event:method:Rules.Match", - "internal/event:event:method:Rules.MatchSimple", - "internal/event:event:method:Rules.Union", - "internal/event:event:method:RulesMap.Add", - "internal/event:event:method:RulesMap.Clone", - "internal/event:event:method:RulesMap.Match", - "internal/event:event:method:RulesMap.MatchSimple", - "internal/event:event:method:RulesMap.Remove", - "internal/event:event:method:S3Key.MarshalXML", - "internal/event:event:method:TargetID.MarshalJSON", - "internal/event:event:method:TargetID.String", - "internal/event:event:method:TargetID.ToARN", - "internal/event:event:method:TargetID.UnmarshalJSON", - "internal/event:event:method:TargetIDSet.Clone", - "internal/event:event:method:TargetIDSet.Difference", - "internal/event:event:method:TargetIDSet.Union", - "internal/event:event:method:TargetList.Add", - "internal/event:event:method:TargetList.Exists", - "internal/event:event:method:TargetList.Init", - "internal/event:event:method:TargetList.List", - "internal/event:event:method:TargetList.Remove", - "internal/event:event:method:TargetList.Send", - "internal/event:event:method:TargetList.Stats", - "internal/event:event:method:TargetList.TargetMap", - "internal/event:event:method:TargetList.Targets", - "internal/event:event:type:ARN", - "internal/event:event:type:Bucket", - "internal/event:event:type:Config", - "internal/event:event:type:ErrARNNotFound", - "internal/event:event:type:ErrDuplicateEventName", - "internal/event:event:type:ErrDuplicateQueueConfiguration", - "internal/event:event:type:ErrFilterNamePrefix", - "internal/event:event:type:ErrFilterNameSuffix", - "internal/event:event:type:ErrInvalidARN", - "internal/event:event:type:ErrInvalidEventName", - "internal/event:event:type:ErrInvalidFilterName", - "internal/event:event:type:ErrInvalidFilterValue", - "internal/event:event:type:ErrUnknownRegion", - "internal/event:event:type:ErrUnsupportedConfiguration", - "internal/event:event:type:Event", - "internal/event:event:type:FilterRule", - "internal/event:event:type:FilterRuleList", - "internal/event:event:type:Identity", - "internal/event:event:type:Log", - "internal/event:event:type:Metadata", - "internal/event:event:type:Name", - "internal/event:event:type:Object", - "internal/event:event:type:Queue", - "internal/event:event:type:Rules", - "internal/event:event:type:RulesMap", - "internal/event:event:type:S3Key", - "internal/event:event:type:Source", - "internal/event:event:type:Stats", - "internal/event:event:type:Target", - "internal/event:event:type:TargetID", - "internal/event:event:type:TargetIDResult", - "internal/event:event:type:TargetIDSet", - "internal/event:event:type:TargetList", - "internal/event:event:type:TargetStat", - "internal/event:event:type:TargetStore", - "internal/grid:grid:const:FlagCRCxxh3", - "internal/grid:grid:const:FlagEOF", - "internal/grid:grid:const:FlagPayloadIsErr", - "internal/grid:grid:const:FlagPayloadIsZero", - "internal/grid:grid:const:FlagStateless", - "internal/grid:grid:const:FlagSubroute", - "internal/grid:grid:const:HandlerBackgroundHealStatus", - "internal/grid:grid:const:HandlerCheckParts", - "internal/grid:grid:const:HandlerCheckParts2", - "internal/grid:grid:const:HandlerCheckParts3", - "internal/grid:grid:const:HandlerClearUploadID", - "internal/grid:grid:const:HandlerConsoleLog", - "internal/grid:grid:const:HandlerDeleteBucket", - "internal/grid:grid:const:HandlerDeleteBucketMetadata", - "internal/grid:grid:const:HandlerDeleteFile", - "internal/grid:grid:const:HandlerDeletePolicy", - "internal/grid:grid:const:HandlerDeleteServiceAccount", - "internal/grid:grid:const:HandlerDeleteUser", - "internal/grid:grid:const:HandlerDeleteVersion", - "internal/grid:grid:const:HandlerDiskInfo", - "internal/grid:grid:const:HandlerGetAllBucketStats", - "internal/grid:grid:const:HandlerGetBandwidth", - "internal/grid:grid:const:HandlerGetBucketStats", - "internal/grid:grid:const:HandlerGetCPUs", - "internal/grid:grid:const:HandlerGetLastDayTierStats", - "internal/grid:grid:const:HandlerGetLocks", - "internal/grid:grid:const:HandlerGetMemInfo", - "internal/grid:grid:const:HandlerGetMetacacheListing", - "internal/grid:grid:const:HandlerGetMetrics", - "internal/grid:grid:const:HandlerGetNetInfo", - "internal/grid:grid:const:HandlerGetOSInfo", - "internal/grid:grid:const:HandlerGetPartitions", - "internal/grid:grid:const:HandlerGetPeerBucketMetrics", - "internal/grid:grid:const:HandlerGetPeerMetrics", - "internal/grid:grid:const:HandlerGetProcInfo", - "internal/grid:grid:const:HandlerGetResourceMetrics", - "internal/grid:grid:const:HandlerGetSRMetrics", - "internal/grid:grid:const:HandlerGetSysConfig", - "internal/grid:grid:const:HandlerGetSysErrors", - "internal/grid:grid:const:HandlerGetSysServices", - "internal/grid:grid:const:HandlerHeadBucket", - "internal/grid:grid:const:HandlerHealBucket", - "internal/grid:grid:const:HandlerListBuckets", - "internal/grid:grid:const:HandlerListDir", - "internal/grid:grid:const:HandlerListen", - "internal/grid:grid:const:HandlerLoadBucketMetadata", - "internal/grid:grid:const:HandlerLoadGroup", - "internal/grid:grid:const:HandlerLoadPolicy", - "internal/grid:grid:const:HandlerLoadPolicyMapping", - "internal/grid:grid:const:HandlerLoadRebalanceMeta", - "internal/grid:grid:const:HandlerLoadServiceAccount", - "internal/grid:grid:const:HandlerLoadTransitionTierConfig", - "internal/grid:grid:const:HandlerLoadUser", - "internal/grid:grid:const:HandlerLockForceUnlock", - "internal/grid:grid:const:HandlerLockLock", - "internal/grid:grid:const:HandlerLockRLock", - "internal/grid:grid:const:HandlerLockRUnlock", - "internal/grid:grid:const:HandlerLockRefresh", - "internal/grid:grid:const:HandlerLockUnlock", - "internal/grid:grid:const:HandlerMakeBucket", - "internal/grid:grid:const:HandlerNSScanner", - "internal/grid:grid:const:HandlerReadAll", - "internal/grid:grid:const:HandlerReadVersion", - "internal/grid:grid:const:HandlerReadXL", - "internal/grid:grid:const:HandlerReloadPoolMeta", - "internal/grid:grid:const:HandlerReloadSiteReplicationConfig", - "internal/grid:grid:const:HandlerRenameData", - "internal/grid:grid:const:HandlerRenameData2", - "internal/grid:grid:const:HandlerRenameDataInline", - "internal/grid:grid:const:HandlerRenameFile", - "internal/grid:grid:const:HandlerRenamePart", - "internal/grid:grid:const:HandlerServerInfo", - "internal/grid:grid:const:HandlerServerVerify", - "internal/grid:grid:const:HandlerSignalService", - "internal/grid:grid:const:HandlerStatVol", - "internal/grid:grid:const:HandlerStopRebalance", - "internal/grid:grid:const:HandlerStorageInfo", - "internal/grid:grid:const:HandlerTrace", - "internal/grid:grid:const:HandlerUpdateMetacacheListing", - "internal/grid:grid:const:HandlerUpdateMetadata", - "internal/grid:grid:const:HandlerWalkDir", - "internal/grid:grid:const:HandlerWriteAll", - "internal/grid:grid:const:HandlerWriteMetadata", - "internal/grid:grid:const:MaxDeadline", - "internal/grid:grid:const:OpAckMux", - "internal/grid:grid:const:OpConnect", - "internal/grid:grid:const:OpConnectMux", - "internal/grid:grid:const:OpConnectResponse", - "internal/grid:grid:const:OpDisconnect", - "internal/grid:grid:const:OpDisconnectClientMux", - "internal/grid:grid:const:OpDisconnectServerMux", - "internal/grid:grid:const:OpMerged", - "internal/grid:grid:const:OpMuxClientMsg", - "internal/grid:grid:const:OpMuxConnectError", - "internal/grid:grid:const:OpMuxServerMsg", - "internal/grid:grid:const:OpPing", - "internal/grid:grid:const:OpPong", - "internal/grid:grid:const:OpRequest", - "internal/grid:grid:const:OpResponse", - "internal/grid:grid:const:OpUnblockClMux", - "internal/grid:grid:const:OpUnblockSrvMux", - "internal/grid:grid:const:RouteLockPath", - "internal/grid:grid:const:RoutePath", - "internal/grid:grid:const:StateConnected", - "internal/grid:grid:const:StateConnecting", - "internal/grid:grid:const:StateConnectionError", - "internal/grid:grid:const:StateShutdown", - "internal/grid:grid:const:StateUnconnected", - "internal/grid:grid:field:Connection.LastPong", - "internal/grid:grid:field:Connection.Local", - "internal/grid:grid:field:Connection.NextID", - "internal/grid:grid:field:Connection.Remote", - "internal/grid:grid:field:Manager.ID", - "internal/grid:grid:field:ManagerOptions.AuthFn", - "internal/grid:grid:field:ManagerOptions.AuthToken", - "internal/grid:grid:field:ManagerOptions.BlockConnect", - "internal/grid:grid:field:ManagerOptions.Dialer", - "internal/grid:grid:field:ManagerOptions.Hosts", - "internal/grid:grid:field:ManagerOptions.Incoming", - "internal/grid:grid:field:ManagerOptions.Local", - "internal/grid:grid:field:ManagerOptions.Outgoing", - "internal/grid:grid:field:ManagerOptions.RoutePath", - "internal/grid:grid:field:ManagerOptions.TraceTo", - "internal/grid:grid:field:Recycler.Recycle", - "internal/grid:grid:field:RemoteClient.Name", - "internal/grid:grid:field:Requester.Request", - "internal/grid:grid:field:Response.Err", - "internal/grid:grid:field:Response.Msg", - "internal/grid:grid:field:StatelessHandler.Handle", - "internal/grid:grid:field:StatelessHandler.OutCapacity", - "internal/grid:grid:field:Stream.Requests", - "internal/grid:grid:field:StreamHandler.Handle", - "internal/grid:grid:field:StreamHandler.InCapacity", - "internal/grid:grid:field:StreamHandler.OutCapacity", - "internal/grid:grid:field:StreamHandler.Subroute", - "internal/grid:grid:field:StreamTypeHandler.InCapacity", - "internal/grid:grid:field:StreamTypeHandler.OutCapacity", - "internal/grid:grid:field:StreamTypeHandler.WithPayload", - "internal/grid:grid:field:Streamer.NewStream", - "internal/grid:grid:field:TestGrid.Hosts", - "internal/grid:grid:field:TestGrid.Listeners", - "internal/grid:grid:field:TestGrid.Managers", - "internal/grid:grid:field:TestGrid.Mux", - "internal/grid:grid:field:TestGrid.Servers", - "internal/grid:grid:field:TypedStream.Requests", - "internal/grid:grid:func:ConnectWS", - "internal/grid:grid:func:ConnectWSWithRoutePath", - "internal/grid:grid:func:GetByteBufferCap", - "internal/grid:grid:func:GetCaller", - "internal/grid:grid:func:GetSubroute", - "internal/grid:grid:func:IsRemoteErr", - "internal/grid:grid:func:NewArrayOf", - "internal/grid:grid:func:NewBytes", - "internal/grid:grid:func:NewBytesCap", - "internal/grid:grid:func:NewBytesWith", - "internal/grid:grid:func:NewBytesWithCopyOf", - "internal/grid:grid:func:NewJSONPool", - "internal/grid:grid:func:NewMSS", - "internal/grid:grid:func:NewMSSWith", - "internal/grid:grid:func:NewManager", - "internal/grid:grid:func:NewNPErr", - "internal/grid:grid:func:NewNoPayload", - "internal/grid:grid:func:NewRemoteErr", - "internal/grid:grid:func:NewRemoteErrString", - "internal/grid:grid:func:NewRemoteErrf", - "internal/grid:grid:func:NewSingleHandler", - "internal/grid:grid:func:NewStream", - "internal/grid:grid:func:NewURLValues", - "internal/grid:grid:func:NewURLValuesWith", - "internal/grid:grid:func:SetupTestGrid", - "internal/grid:grid:func:WriterToChannel", - "internal/grid:grid:method:Array.Append", - "internal/grid:grid:method:Array.MarshalMsg", - "internal/grid:grid:method:Array.Msgsize", - "internal/grid:grid:method:Array.Recycle", - "internal/grid:grid:method:Array.Set", - "internal/grid:grid:method:Array.UnmarshalMsg", - "internal/grid:grid:method:Array.Value", - "internal/grid:grid:method:ArrayOf.New", - "internal/grid:grid:method:ArrayOf.NewWith", - "internal/grid:grid:method:Bytes.MarshalMsg", - "internal/grid:grid:method:Bytes.Msgsize", - "internal/grid:grid:method:Bytes.Recycle", - "internal/grid:grid:method:Bytes.UnmarshalMsg", - "internal/grid:grid:method:Connection.NewStream", - "internal/grid:grid:method:Connection.Request", - "internal/grid:grid:method:Connection.State", - "internal/grid:grid:method:Connection.Stats", - "internal/grid:grid:method:Connection.String", - "internal/grid:grid:method:Connection.StringReverse", - "internal/grid:grid:method:Connection.Subroute", - "internal/grid:grid:method:Connection.WaitForConnect", - "internal/grid:grid:method:ContextDialer.DialContext", - "internal/grid:grid:method:ErrResponse.Error", - "internal/grid:grid:method:Flags.Clear", - "internal/grid:grid:method:Flags.DecodeMsg", - "internal/grid:grid:method:Flags.EncodeMsg", - "internal/grid:grid:method:Flags.MarshalMsg", - "internal/grid:grid:method:Flags.Msgsize", - "internal/grid:grid:method:Flags.Set", - "internal/grid:grid:method:Flags.String", - "internal/grid:grid:method:Flags.UnmarshalMsg", - "internal/grid:grid:method:HandlerID.DecodeMsg", - "internal/grid:grid:method:HandlerID.EncodeMsg", - "internal/grid:grid:method:HandlerID.MarshalMsg", - "internal/grid:grid:method:HandlerID.Msgsize", - "internal/grid:grid:method:HandlerID.String", - "internal/grid:grid:method:HandlerID.UnmarshalMsg", - "internal/grid:grid:method:JSON.MarshalMsg", - "internal/grid:grid:method:JSON.Msgsize", - "internal/grid:grid:method:JSON.Recycle", - "internal/grid:grid:method:JSON.Set", - "internal/grid:grid:method:JSON.UnmarshalMsg", - "internal/grid:grid:method:JSON.Value", - "internal/grid:grid:method:JSON.ValueOrZero", - "internal/grid:grid:method:JSONPool.NewJSON", - "internal/grid:grid:method:JSONPool.NewJSONWith", - "internal/grid:grid:method:MSS.Get", - "internal/grid:grid:method:MSS.MarshalMsg", - "internal/grid:grid:method:MSS.Msgsize", - "internal/grid:grid:method:MSS.Recycle", - "internal/grid:grid:method:MSS.Set", - "internal/grid:grid:method:MSS.ToQuery", - "internal/grid:grid:method:MSS.UnmarshalMsg", - "internal/grid:grid:method:Manager.AddToMux", - "internal/grid:grid:method:Manager.ConnStats", - "internal/grid:grid:method:Manager.Connection", - "internal/grid:grid:method:Manager.Handler", - "internal/grid:grid:method:Manager.HostName", - "internal/grid:grid:method:Manager.IncomingConn", - "internal/grid:grid:method:Manager.RegisterSingleHandler", - "internal/grid:grid:method:Manager.RegisterStreamingHandler", - "internal/grid:grid:method:Manager.Targets", - "internal/grid:grid:method:NoPayload.MarshalMsg", - "internal/grid:grid:method:NoPayload.Msgsize", - "internal/grid:grid:method:NoPayload.Recycle", - "internal/grid:grid:method:NoPayload.UnmarshalMsg", - "internal/grid:grid:method:Op.DecodeMsg", - "internal/grid:grid:method:Op.EncodeMsg", - "internal/grid:grid:method:Op.MarshalMsg", - "internal/grid:grid:method:Op.Msgsize", - "internal/grid:grid:method:Op.String", - "internal/grid:grid:method:Op.UnmarshalMsg", - "internal/grid:grid:method:RemoteErr.Error", - "internal/grid:grid:method:RemoteErr.Is", - "internal/grid:grid:method:SingleHandler.AllowCallRequestPool", - "internal/grid:grid:method:SingleHandler.Call", - "internal/grid:grid:method:SingleHandler.IgnoreNilConn", - "internal/grid:grid:method:SingleHandler.NewRequest", - "internal/grid:grid:method:SingleHandler.NewResponse", - "internal/grid:grid:method:SingleHandler.PutResponse", - "internal/grid:grid:method:SingleHandler.Register", - "internal/grid:grid:method:SingleHandler.WithSharedResponse", - "internal/grid:grid:method:Stream.Done", - "internal/grid:grid:method:Stream.Err", - "internal/grid:grid:method:Stream.Results", - "internal/grid:grid:method:Stream.Send", - "internal/grid:grid:method:StreamTypeHandler.Call", - "internal/grid:grid:method:StreamTypeHandler.NewPayload", - "internal/grid:grid:method:StreamTypeHandler.NewRequest", - "internal/grid:grid:method:StreamTypeHandler.NewResponse", - "internal/grid:grid:method:StreamTypeHandler.PutRequest", - "internal/grid:grid:method:StreamTypeHandler.PutResponse", - "internal/grid:grid:method:StreamTypeHandler.Register", - "internal/grid:grid:method:StreamTypeHandler.RegisterNoInput", - "internal/grid:grid:method:StreamTypeHandler.RegisterNoPayload", - "internal/grid:grid:method:StreamTypeHandler.WithInCapacity", - "internal/grid:grid:method:StreamTypeHandler.WithOutCapacity", - "internal/grid:grid:method:StreamTypeHandler.WithSharedResponse", - "internal/grid:grid:method:Subroute.NewStream", - "internal/grid:grid:method:Subroute.Request", - "internal/grid:grid:method:Subroute.Subroute", - "internal/grid:grid:method:TestGrid.Cleanup", - "internal/grid:grid:method:TestGrid.WaitAllConnect", - "internal/grid:grid:method:TypedStream.Results", - "internal/grid:grid:method:URLValues.MarshalMsg", - "internal/grid:grid:method:URLValues.Msgsize", - "internal/grid:grid:method:URLValues.Recycle", - "internal/grid:grid:method:URLValues.UnmarshalMsg", - "internal/grid:grid:method:URLValues.Values", - "internal/grid:grid:method:connectReq.DecodeMsg", - "internal/grid:grid:method:connectReq.EncodeMsg", - "internal/grid:grid:method:connectReq.MarshalMsg", - "internal/grid:grid:method:connectReq.Msgsize", - "internal/grid:grid:method:connectReq.Op", - "internal/grid:grid:method:connectReq.UnmarshalMsg", - "internal/grid:grid:method:connectResp.DecodeMsg", - "internal/grid:grid:method:connectResp.EncodeMsg", - "internal/grid:grid:method:connectResp.MarshalMsg", - "internal/grid:grid:method:connectResp.Msgsize", - "internal/grid:grid:method:connectResp.Op", - "internal/grid:grid:method:connectResp.UnmarshalMsg", - "internal/grid:grid:method:debugMsg.String", - "internal/grid:grid:method:message.DecodeMsg", - "internal/grid:grid:method:message.EncodeMsg", - "internal/grid:grid:method:message.MarshalMsg", - "internal/grid:grid:method:message.Msgsize", - "internal/grid:grid:method:message.String", - "internal/grid:grid:method:message.UnmarshalMsg", - "internal/grid:grid:method:muxClient.RequestStateless", - "internal/grid:grid:method:muxClient.RequestStream", - "internal/grid:grid:method:muxConnectError.DecodeMsg", - "internal/grid:grid:method:muxConnectError.EncodeMsg", - "internal/grid:grid:method:muxConnectError.MarshalMsg", - "internal/grid:grid:method:muxConnectError.Msgsize", - "internal/grid:grid:method:muxConnectError.Op", - "internal/grid:grid:method:muxConnectError.UnmarshalMsg", - "internal/grid:grid:method:pingMsg.DecodeMsg", - "internal/grid:grid:method:pingMsg.EncodeMsg", - "internal/grid:grid:method:pingMsg.MarshalMsg", - "internal/grid:grid:method:pingMsg.Msgsize", - "internal/grid:grid:method:pingMsg.Op", - "internal/grid:grid:method:pingMsg.UnmarshalMsg", - "internal/grid:grid:method:pongMsg.DecodeMsg", - "internal/grid:grid:method:pongMsg.EncodeMsg", - "internal/grid:grid:method:pongMsg.MarshalMsg", - "internal/grid:grid:method:pongMsg.Msgsize", - "internal/grid:grid:method:pongMsg.Op", - "internal/grid:grid:method:pongMsg.UnmarshalMsg", - "internal/grid:grid:method:subHandlerID.String", - "internal/grid:grid:method:writerWrapper.Write", - "internal/grid:grid:type:Array", - "internal/grid:grid:type:ArrayOf", - "internal/grid:grid:type:AuthFn", - "internal/grid:grid:type:Bytes", - "internal/grid:grid:type:ConnDialer", - "internal/grid:grid:type:Connection", - "internal/grid:grid:type:ContextDialer", - "internal/grid:grid:type:ErrResponse", - "internal/grid:grid:type:Flags", - "internal/grid:grid:type:HandlerID", - "internal/grid:grid:type:JSON", - "internal/grid:grid:type:JSONPool", - "internal/grid:grid:type:MSS", - "internal/grid:grid:type:Manager", - "internal/grid:grid:type:ManagerOptions", - "internal/grid:grid:type:NoPayload", - "internal/grid:grid:type:Op", - "internal/grid:grid:type:Recycler", - "internal/grid:grid:type:RemoteClient", - "internal/grid:grid:type:RemoteErr", - "internal/grid:grid:type:Requester", - "internal/grid:grid:type:Response", - "internal/grid:grid:type:RoundTripper", - "internal/grid:grid:type:SingleHandler", - "internal/grid:grid:type:SingleHandlerFn", - "internal/grid:grid:type:State", - "internal/grid:grid:type:StatelessHandler", - "internal/grid:grid:type:StatelessHandlerFn", - "internal/grid:grid:type:Stream", - "internal/grid:grid:type:StreamHandler", - "internal/grid:grid:type:StreamHandlerFn", - "internal/grid:grid:type:StreamTypeHandler", - "internal/grid:grid:type:Streamer", - "internal/grid:grid:type:Subroute", - "internal/grid:grid:type:TestGrid", - "internal/grid:grid:type:TraceParamsKey", - "internal/grid:grid:type:TypedStream", - "internal/grid:grid:type:URLValues", - "internal/grid:grid:type:ValidateAuthFn", - "internal/grid:grid:type:ValidateTokenFn", - "internal/grid:grid:var:ErrDisconnected", - "internal/grid:grid:var:ErrHandlerAlreadyExists", - "internal/grid:grid:var:ErrIncorrectSequence", - "internal/grid:grid:var:ErrUnknownHandler", - "internal/grid:grid:var:GetByteBuffer", - "internal/grid:grid:var:PutByteBuffer", - "internal/handlers:handlers:const:EnvTrustedProxies", - "internal/handlers:handlers:const:EnvXFFHeader", - "internal/handlers:handlers:const:TrustNoProxies", - "internal/handlers:handlers:field:Forwarder.ErrorHandler", - "internal/handlers:handlers:field:Forwarder.Logger", - "internal/handlers:handlers:field:Forwarder.PassHost", - "internal/handlers:handlers:field:Forwarder.RoundTripper", - "internal/handlers:handlers:func:ConfigureSourceIPTrust", - "internal/handlers:handlers:func:GetSourceIP", - "internal/handlers:handlers:func:GetSourceIPFromHeaders", - "internal/handlers:handlers:func:GetSourceIPRaw", - "internal/handlers:handlers:func:GetSourceScheme", - "internal/handlers:handlers:func:NewForwarder", - "internal/handlers:handlers:func:TrustsForwardedHeaders", - "internal/handlers:handlers:method:Forwarder.ServeHTTP", - "internal/handlers:handlers:method:bufPool.Get", - "internal/handlers:handlers:method:bufPool.Put", - "internal/handlers:handlers:method:headerRewriter.Rewrite", - "internal/handlers:handlers:type:Forwarder", - "internal/hash/sha256:sha256:const:Size", - "internal/hash/sha256:sha256:func:New", - "internal/hash/sha256:sha256:func:Sum256", - "internal/hash:hash:const:ChecksumCRC32", - "internal/hash:hash:const:ChecksumCRC32C", - "internal/hash:hash:const:ChecksumCRC64NVME", - "internal/hash:hash:const:ChecksumFullObject", - "internal/hash:hash:const:ChecksumIncludesMultipart", - "internal/hash:hash:const:ChecksumInvalid", - "internal/hash:hash:const:ChecksumMultipart", - "internal/hash:hash:const:ChecksumNone", - "internal/hash:hash:const:ChecksumSHA1", - "internal/hash:hash:const:ChecksumSHA256", - "internal/hash:hash:const:ChecksumTrailing", - "internal/hash:hash:const:MinIOMultipartChecksum", - "internal/hash:hash:const:MinIOMultipartChecksumType", - "internal/hash:hash:field:BadDigest.CalculatedMD5", - "internal/hash:hash:field:BadDigest.ExpectedMD5", - "internal/hash:hash:field:Checksum.Encoded", - "internal/hash:hash:field:Checksum.Raw", - "internal/hash:hash:field:Checksum.Type", - "internal/hash:hash:field:Checksum.WantParts", - "internal/hash:hash:field:ChecksumMismatch.Got", - "internal/hash:hash:field:ChecksumMismatch.Want", - "internal/hash:hash:field:Options.ActualSize", - "internal/hash:hash:field:Options.DisableMD5", - "internal/hash:hash:field:Options.ForceMD5", - "internal/hash:hash:field:Options.MD5Hex", - "internal/hash:hash:field:Options.SHA256Hex", - "internal/hash:hash:field:Options.Size", - "internal/hash:hash:field:Reader.ServerSideChecksumResult", - "internal/hash:hash:field:Reader.ServerSideChecksumType", - "internal/hash:hash:field:Reader.ServerSideHasher", - "internal/hash:hash:field:SHA256Mismatch.CalculatedSHA256", - "internal/hash:hash:field:SHA256Mismatch.ExpectedSHA256", - "internal/hash:hash:field:SizeMismatch.Got", - "internal/hash:hash:field:SizeMismatch.Want", - "internal/hash:hash:field:SizeTooLarge.Got", - "internal/hash:hash:field:SizeTooLarge.Want", - "internal/hash:hash:field:SizeTooSmall.Got", - "internal/hash:hash:field:SizeTooSmall.Want", - "internal/hash:hash:func:AddChecksumHeader", - "internal/hash:hash:func:ChecksumFromBytes", - "internal/hash:hash:func:ChecksumStringToType", - "internal/hash:hash:func:GetContentChecksum", - "internal/hash:hash:func:IsChecksumMismatch", - "internal/hash:hash:func:NewChecker", - "internal/hash:hash:func:NewChecksumFromData", - "internal/hash:hash:func:NewChecksumHeader", - "internal/hash:hash:func:NewChecksumString", - "internal/hash:hash:func:NewChecksumType", - "internal/hash:hash:func:NewChecksumWithType", - "internal/hash:hash:func:NewReader", - "internal/hash:hash:func:NewReaderWithOpts", - "internal/hash:hash:func:ReadCheckSums", - "internal/hash:hash:func:ReadPartCheckSums", - "internal/hash:hash:func:TransferChecksumHeader", - "internal/hash:hash:method:BadDigest.Error", - "internal/hash:hash:method:Checker.Close", - "internal/hash:hash:method:Checker.Read", - "internal/hash:hash:method:Checksum.AddPart", - "internal/hash:hash:method:Checksum.AppendTo", - "internal/hash:hash:method:Checksum.AsMap", - "internal/hash:hash:method:Checksum.Equal", - "internal/hash:hash:method:Checksum.Matches", - "internal/hash:hash:method:Checksum.Valid", - "internal/hash:hash:method:ChecksumMismatch.Error", - "internal/hash:hash:method:ChecksumType.Base", - "internal/hash:hash:method:ChecksumType.CanMerge", - "internal/hash:hash:method:ChecksumType.FullObjectRequested", - "internal/hash:hash:method:ChecksumType.Hasher", - "internal/hash:hash:method:ChecksumType.Is", - "internal/hash:hash:method:ChecksumType.IsMultipartComposite", - "internal/hash:hash:method:ChecksumType.IsSet", - "internal/hash:hash:method:ChecksumType.Key", - "internal/hash:hash:method:ChecksumType.ObjType", - "internal/hash:hash:method:ChecksumType.RawByteLen", - "internal/hash:hash:method:ChecksumType.String", - "internal/hash:hash:method:ChecksumType.StringFull", - "internal/hash:hash:method:ChecksumType.Trailing", - "internal/hash:hash:method:Reader.ActualSize", - "internal/hash:hash:method:Reader.AddChecksum", - "internal/hash:hash:method:Reader.AddChecksumNoTrailer", - "internal/hash:hash:method:Reader.AddNonTrailingChecksum", - "internal/hash:hash:method:Reader.AddServerSideChecksumHasher", - "internal/hash:hash:method:Reader.Checksum", - "internal/hash:hash:method:Reader.Close", - "internal/hash:hash:method:Reader.ContentCRC", - "internal/hash:hash:method:Reader.ContentCRCType", - "internal/hash:hash:method:Reader.ETag", - "internal/hash:hash:method:Reader.MD5Current", - "internal/hash:hash:method:Reader.Read", - "internal/hash:hash:method:Reader.SHA256", - "internal/hash:hash:method:Reader.SHA256HexString", - "internal/hash:hash:method:Reader.SetExpectedMax", - "internal/hash:hash:method:Reader.SetExpectedMin", - "internal/hash:hash:method:Reader.Size", - "internal/hash:hash:method:SHA256Mismatch.Error", - "internal/hash:hash:method:SizeMismatch.Error", - "internal/hash:hash:method:SizeTooLarge.Error", - "internal/hash:hash:method:SizeTooSmall.Error", - "internal/hash:hash:type:BadDigest", - "internal/hash:hash:type:Checker", - "internal/hash:hash:type:Checksum", - "internal/hash:hash:type:ChecksumMismatch", - "internal/hash:hash:type:ChecksumType", - "internal/hash:hash:type:Options", - "internal/hash:hash:type:Reader", - "internal/hash:hash:type:SHA256Mismatch", - "internal/hash:hash:type:SizeMismatch", - "internal/hash:hash:type:SizeTooLarge", - "internal/hash:hash:type:SizeTooSmall", - "internal/hash:hash:var:BaseChecksumTypes", - "internal/hash:hash:var:ErrInvalidChecksum", - "internal/http:http:const:AcceptRanges", - "internal/http:http:const:Action", - "internal/http:http:const:AmzACL", - "internal/http:http:const:AmzAccessKeyID", - "internal/http:http:const:AmzAlgorithm", - "internal/http:http:const:AmzBucketRegion", - "internal/http:http:const:AmzBucketReplicationStatus", - "internal/http:http:const:AmzChecksumAlgo", - "internal/http:http:const:AmzChecksumCRC32", - "internal/http:http:const:AmzChecksumCRC32C", - "internal/http:http:const:AmzChecksumCRC64NVME", - "internal/http:http:const:AmzChecksumMode", - "internal/http:http:const:AmzChecksumSHA1", - "internal/http:http:const:AmzChecksumSHA256", - "internal/http:http:const:AmzChecksumType", - "internal/http:http:const:AmzChecksumTypeComposite", - "internal/http:http:const:AmzChecksumTypeFullObject", - "internal/http:http:const:AmzContentSha256", - "internal/http:http:const:AmzCopySource", - "internal/http:http:const:AmzCopySourceIfMatch", - "internal/http:http:const:AmzCopySourceIfModifiedSince", - "internal/http:http:const:AmzCopySourceIfNoneMatch", - "internal/http:http:const:AmzCopySourceIfUnmodifiedSince", - "internal/http:http:const:AmzCopySourceRange", - "internal/http:http:const:AmzCopySourceVersionID", - "internal/http:http:const:AmzCredential", - "internal/http:http:const:AmzDate", - "internal/http:http:const:AmzDecodedContentLength", - "internal/http:http:const:AmzDeleteMarker", - "internal/http:http:const:AmzEncryptionAES", - "internal/http:http:const:AmzEncryptionKMS", - "internal/http:http:const:AmzExpiration", - "internal/http:http:const:AmzExpires", - "internal/http:http:const:AmzFwdErrorCode", - "internal/http:http:const:AmzFwdErrorMessage", - "internal/http:http:const:AmzFwdHeaderAcceptRanges", - "internal/http:http:const:AmzFwdHeaderCacheControl", - "internal/http:http:const:AmzFwdHeaderChecksumCrc32", - "internal/http:http:const:AmzFwdHeaderChecksumCrc32c", - "internal/http:http:const:AmzFwdHeaderChecksumSha1", - "internal/http:http:const:AmzFwdHeaderChecksumSha256", - "internal/http:http:const:AmzFwdHeaderContentDisposition", - "internal/http:http:const:AmzFwdHeaderContentEncoding", - "internal/http:http:const:AmzFwdHeaderContentLanguage", - "internal/http:http:const:AmzFwdHeaderContentRange", - "internal/http:http:const:AmzFwdHeaderContentType", - "internal/http:http:const:AmzFwdHeaderDeleteMarker", - "internal/http:http:const:AmzFwdHeaderETag", - "internal/http:http:const:AmzFwdHeaderExpiration", - "internal/http:http:const:AmzFwdHeaderExpires", - "internal/http:http:const:AmzFwdHeaderLastModified", - "internal/http:http:const:AmzFwdHeaderMPPartsCount", - "internal/http:http:const:AmzFwdHeaderObjectLockLegalHold", - "internal/http:http:const:AmzFwdHeaderObjectLockMode", - "internal/http:http:const:AmzFwdHeaderObjectLockRetainUntil", - "internal/http:http:const:AmzFwdHeaderReplicationStatus", - "internal/http:http:const:AmzFwdHeaderSSE", - "internal/http:http:const:AmzFwdHeaderSSEC", - "internal/http:http:const:AmzFwdHeaderSSECMD5", - "internal/http:http:const:AmzFwdHeaderSSEKMSID", - "internal/http:http:const:AmzFwdHeaderStorageClass", - "internal/http:http:const:AmzFwdHeaderTaggingCount", - "internal/http:http:const:AmzFwdHeaderVersionID", - "internal/http:http:const:AmzFwdStatus", - "internal/http:http:const:AmzMaxParts", - "internal/http:http:const:AmzMetaName", - "internal/http:http:const:AmzMetaUUID", - "internal/http:http:const:AmzMetaUnencryptedContentLength", - "internal/http:http:const:AmzMetaUnencryptedContentMD5", - "internal/http:http:const:AmzMetadataDirective", - "internal/http:http:const:AmzMpPartsCount", - "internal/http:http:const:AmzObjectAttributes", - "internal/http:http:const:AmzObjectLockBypassGovernance", - "internal/http:http:const:AmzObjectLockEnabled", - "internal/http:http:const:AmzObjectLockLegalHold", - "internal/http:http:const:AmzObjectLockMode", - "internal/http:http:const:AmzObjectLockRetainUntilDate", - "internal/http:http:const:AmzObjectTagging", - "internal/http:http:const:AmzPartNumberMarker", - "internal/http:http:const:AmzRequestHostID", - "internal/http:http:const:AmzRequestID", - "internal/http:http:const:AmzRequestRoute", - "internal/http:http:const:AmzRequestToken", - "internal/http:http:const:AmzRestore", - "internal/http:http:const:AmzRestoreExpiryDays", - "internal/http:http:const:AmzRestoreOutputPath", - "internal/http:http:const:AmzRestoreRequestDate", - "internal/http:http:const:AmzSecurityToken", - "internal/http:http:const:AmzServerSideEncryption", - "internal/http:http:const:AmzServerSideEncryptionCopyCustomerAlgorithm", - "internal/http:http:const:AmzServerSideEncryptionCopyCustomerKey", - "internal/http:http:const:AmzServerSideEncryptionCopyCustomerKeyMD5", - "internal/http:http:const:AmzServerSideEncryptionCustomerAlgorithm", - "internal/http:http:const:AmzServerSideEncryptionCustomerKey", - "internal/http:http:const:AmzServerSideEncryptionCustomerKeyMD5", - "internal/http:http:const:AmzServerSideEncryptionKmsContext", - "internal/http:http:const:AmzServerSideEncryptionKmsID", - "internal/http:http:const:AmzSignature", - "internal/http:http:const:AmzSignatureV2", - "internal/http:http:const:AmzSignedHeaders", - "internal/http:http:const:AmzSnowballExtract", - "internal/http:http:const:AmzStorageClass", - "internal/http:http:const:AmzTagCount", - "internal/http:http:const:AmzTagDirective", - "internal/http:http:const:AmzTrailer", - "internal/http:http:const:AmzVersionID", - "internal/http:http:const:AmzWriteOffsetBytes", - "internal/http:http:const:Authorization", - "internal/http:http:const:CacheControl", - "internal/http:http:const:Checksum", - "internal/http:http:const:Connection", - "internal/http:http:const:ContentDisposition", - "internal/http:http:const:ContentEncoding", - "internal/http:http:const:ContentLanguage", - "internal/http:http:const:ContentLength", - "internal/http:http:const:ContentMD5", - "internal/http:http:const:ContentRange", - "internal/http:http:const:ContentType", - "internal/http:http:const:Date", - "internal/http:http:const:DefaultIdleTimeout", - "internal/http:http:const:DefaultMaxHeaderBytes", - "internal/http:http:const:DefaultReadHeaderTimeout", - "internal/http:http:const:ETag", - "internal/http:http:const:Expires", - "internal/http:http:const:IfMatch", - "internal/http:http:const:IfModifiedSince", - "internal/http:http:const:IfNoneMatch", - "internal/http:http:const:IfUnmodifiedSince", - "internal/http:http:const:LastModified", - "internal/http:http:const:Location", - "internal/http:http:const:MinIOCheckDMReplicationReady", - "internal/http:http:const:MinIOCompressed", - "internal/http:http:const:MinIODeleteMarkerReplicationStatus", - "internal/http:http:const:MinIODeleteReplicationStatus", - "internal/http:http:const:MinIOForceCreate", - "internal/http:http:const:MinIOForceDelete", - "internal/http:http:const:MinIOHealingDrives", - "internal/http:http:const:MinIOLifecycleCfgUpdatedAt", - "internal/http:http:const:MinIOPeerCall", - "internal/http:http:const:MinIOReadQuorum", - "internal/http:http:const:MinIOReplicationActualObjectSize", - "internal/http:http:const:MinIOReplicationResetStatus", - "internal/http:http:const:MinIOServerStatus", - "internal/http:http:const:MinIOSnowballIgnoreDirs", - "internal/http:http:const:MinIOSnowballIgnoreErrors", - "internal/http:http:const:MinIOSnowballPrefix", - "internal/http:http:const:MinIOSourceDeleteMarker", - "internal/http:http:const:MinIOSourceDeleteMarkerDelete", - "internal/http:http:const:MinIOSourceETag", - "internal/http:http:const:MinIOSourceMTime", - "internal/http:http:const:MinIOSourceObjectLegalHoldTimestamp", - "internal/http:http:const:MinIOSourceObjectRetentionTimestamp", - "internal/http:http:const:MinIOSourceProxyRequest", - "internal/http:http:const:MinIOSourceReplicationCheck", - "internal/http:http:const:MinIOSourceReplicationRequest", - "internal/http:http:const:MinIOSourceTaggingTimestamp", - "internal/http:http:const:MinIOStorageClassDefaults", - "internal/http:http:const:MinIOTaggingProxied", - "internal/http:http:const:MinIOTargetReplicationReady", - "internal/http:http:const:MinIOTransition", - "internal/http:http:const:MinIOVersion", - "internal/http:http:const:MinIOWriteQuorum", - "internal/http:http:const:MinioDeploymentID", - "internal/http:http:const:ObjectParts", - "internal/http:http:const:ObjectSize", - "internal/http:http:const:PartNumber", - "internal/http:http:const:Range", - "internal/http:http:const:ReadBufferSize", - "internal/http:http:const:RetryAfter", - "internal/http:http:const:ServerInfo", - "internal/http:http:const:StorageClass", - "internal/http:http:const:SubnetAPIKey", - "internal/http:http:const:UploadID", - "internal/http:http:const:VersionID", - "internal/http:http:const:WebhookEventPayloadCount", - "internal/http:http:const:WriteBufferSize", - "internal/http:http:const:XCache", - "internal/http:http:const:XCacheLookup", - "internal/http:http:field:ConnSettings.CipherSuites", - "internal/http:http:field:ConnSettings.CurvePreferences", - "internal/http:http:field:ConnSettings.DialContext", - "internal/http:http:field:ConnSettings.DialTimeout", - "internal/http:http:field:ConnSettings.EnableHTTP2", - "internal/http:http:field:ConnSettings.LookupHost", - "internal/http:http:field:ConnSettings.RootCAs", - "internal/http:http:field:ConnSettings.TCPOptions", - "internal/http:http:field:RequestRecorder.LogBody", - "internal/http:http:field:ResponseRecorder.LogAllBody", - "internal/http:http:field:ResponseRecorder.LogErrBody", - "internal/http:http:field:ResponseRecorder.StartTime", - "internal/http:http:field:ResponseRecorder.StatusCode", - "internal/http:http:field:Server.Addrs", - "internal/http:http:field:Server.TCPOptions", - "internal/http:http:field:TCPOptions.DriveOPTimeout", - "internal/http:http:field:TCPOptions.IdleTimeout", - "internal/http:http:field:TCPOptions.Interface", - "internal/http:http:field:TCPOptions.NoDelay", - "internal/http:http:field:TCPOptions.RecvBufSize", - "internal/http:http:field:TCPOptions.SendBufSize", - "internal/http:http:field:TCPOptions.Trace", - "internal/http:http:field:TCPOptions.UserTimeout", - "internal/http:http:func:CheckPortAvailability", - "internal/http:http:func:DialContextWithLookupHost", - "internal/http:http:func:DrainBody", - "internal/http:http:func:Flush", - "internal/http:http:func:NewInternodeDialContext", - "internal/http:http:func:NewResponseRecorder", - "internal/http:http:func:NewServer", - "internal/http:http:func:SetDeploymentID", - "internal/http:http:func:SetMinIOVersion", - "internal/http:http:func:WithUserAgent", - "internal/http:http:method:ConnSettings.NewCustomHTTPProxyTransport", - "internal/http:http:method:ConnSettings.NewHTTPTransportWithClientCerts", - "internal/http:http:method:ConnSettings.NewHTTPTransportWithTimeout", - "internal/http:http:method:ConnSettings.NewInternodeHTTPTransport", - "internal/http:http:method:ConnSettings.NewRemoteTargetHTTPTransport", - "internal/http:http:method:RequestRecorder.Close", - "internal/http:http:method:RequestRecorder.Data", - "internal/http:http:method:RequestRecorder.Read", - "internal/http:http:method:RequestRecorder.Size", - "internal/http:http:method:ResponseRecorder.Body", - "internal/http:http:method:ResponseRecorder.Flush", - "internal/http:http:method:ResponseRecorder.HeaderSize", - "internal/http:http:method:ResponseRecorder.Hijack", - "internal/http:http:method:ResponseRecorder.ReadFrom", - "internal/http:http:method:ResponseRecorder.Size", - "internal/http:http:method:ResponseRecorder.TTFB", - "internal/http:http:method:ResponseRecorder.Write", - "internal/http:http:method:ResponseRecorder.WriteHeader", - "internal/http:http:method:Server.GetRequestCount", - "internal/http:http:method:Server.Init", - "internal/http:http:method:Server.Shutdown", - "internal/http:http:method:Server.UseBaseContext", - "internal/http:http:method:Server.UseCustomLogger", - "internal/http:http:method:Server.UseHandler", - "internal/http:http:method:Server.UseIdleTimeout", - "internal/http:http:method:Server.UseReadHeaderTimeout", - "internal/http:http:method:Server.UseReadTimeout", - "internal/http:http:method:Server.UseTCPOptions", - "internal/http:http:method:Server.UseTLSConfig", - "internal/http:http:method:Server.UseWriteTimeout", - "internal/http:http:method:TCPOptions.ForWebsocket", - "internal/http:http:method:httpListener.Accept", - "internal/http:http:method:httpListener.Addr", - "internal/http:http:method:httpListener.Addrs", - "internal/http:http:method:httpListener.Close", - "internal/http:http:method:uaTransport.RoundTrip", - "internal/http:http:type:ConnSettings", - "internal/http:http:type:DialContext", - "internal/http:http:type:LookupHost", - "internal/http:http:type:RequestRecorder", - "internal/http:http:type:ResponseRecorder", - "internal/http:http:type:Server", - "internal/http:http:type:TCPOptions", - "internal/http:http:var:ErrNotImplemented", - "internal/http:http:var:GlobalDeploymentID", - "internal/http:http:var:GlobalMinIOVersion", - "internal/ioutil:ioutil:const:DirectioAlignSize", - "internal/ioutil:ioutil:const:LargeBlock", - "internal/ioutil:ioutil:const:MediumBlock", - "internal/ioutil:ioutil:const:SmallBlock", - "internal/ioutil:ioutil:field:HardLimitedReader.N", - "internal/ioutil:ioutil:field:HardLimitedReader.R", - "internal/ioutil:ioutil:func:AppendFile", - "internal/ioutil:ioutil:func:Copy", - "internal/ioutil:ioutil:func:CopyAligned", - "internal/ioutil:ioutil:func:DiscardReader", - "internal/ioutil:ioutil:func:HardLimitReader", - "internal/ioutil:ioutil:func:LimitedWriter", - "internal/ioutil:ioutil:func:NewAlignedBytePool", - "internal/ioutil:ioutil:func:NewDeadlineWorker", - "internal/ioutil:ioutil:func:NewDeadlineWriter", - "internal/ioutil:ioutil:func:NewSkipReader", - "internal/ioutil:ioutil:func:NopCloser", - "internal/ioutil:ioutil:func:ReadFile", - "internal/ioutil:ioutil:func:ReadFileWithFileInfo", - "internal/ioutil:ioutil:func:SafeClose", - "internal/ioutil:ioutil:func:SameFile", - "internal/ioutil:ioutil:func:WaitPipe", - "internal/ioutil:ioutil:func:WithDeadline", - "internal/ioutil:ioutil:func:WriteOnClose", - "internal/ioutil:ioutil:method:AlignedBytePool.Get", - "internal/ioutil:ioutil:method:AlignedBytePool.Put", - "internal/ioutil:ioutil:method:DeadlineWorker.Run", - "internal/ioutil:ioutil:method:DeadlineWriter.Close", - "internal/ioutil:ioutil:method:DeadlineWriter.Write", - "internal/ioutil:ioutil:method:HardLimitedReader.Read", - "internal/ioutil:ioutil:method:LimitWriter.Close", - "internal/ioutil:ioutil:method:LimitWriter.Write", - "internal/ioutil:ioutil:method:PipeReader.CloseWithError", - "internal/ioutil:ioutil:method:PipeWriter.CloseWithError", - "internal/ioutil:ioutil:method:SkipReader.Read", - "internal/ioutil:ioutil:method:WriteOnCloser.Close", - "internal/ioutil:ioutil:method:WriteOnCloser.HasWritten", - "internal/ioutil:ioutil:method:WriteOnCloser.Write", - "internal/ioutil:ioutil:method:discard.Write", - "internal/ioutil:ioutil:method:nopCloser.Close", - "internal/ioutil:ioutil:type:AlignedBytePool", - "internal/ioutil:ioutil:type:DeadlineWorker", - "internal/ioutil:ioutil:type:DeadlineWriter", - "internal/ioutil:ioutil:type:HardLimitedReader", - "internal/ioutil:ioutil:type:LimitWriter", - "internal/ioutil:ioutil:type:PipeReader", - "internal/ioutil:ioutil:type:PipeWriter", - "internal/ioutil:ioutil:type:SkipReader", - "internal/ioutil:ioutil:type:WriteOnCloser", - "internal/ioutil:ioutil:var:Discard", - "internal/ioutil:ioutil:var:ErrOverread", - "internal/ioutil:ioutil:var:ODirectPoolLarge", - "internal/ioutil:ioutil:var:ODirectPoolMedium", - "internal/ioutil:ioutil:var:ODirectPoolSmall", - "internal/ioutil:ioutil:var:OpenFileDirectIO", - "internal/ioutil:ioutil:var:OsOpen", - "internal/ioutil:ioutil:var:OsOpenFile", - "internal/jwt:jwt:field:MapClaims.AccessKey", - "internal/jwt:jwt:field:SigningMethodHMAC.Hash", - "internal/jwt:jwt:field:SigningMethodHMAC.HasherPool", - "internal/jwt:jwt:field:SigningMethodHMAC.Name", - "internal/jwt:jwt:field:StandardClaims.AccessKey", - "internal/jwt:jwt:func:NewMapClaims", - "internal/jwt:jwt:func:NewStandardClaims", - "internal/jwt:jwt:func:ParseUnverifiedMapClaims", - "internal/jwt:jwt:func:ParseUnverifiedStandardClaims", - "internal/jwt:jwt:func:ParseWithClaims", - "internal/jwt:jwt:func:ParseWithStandardClaims", - "internal/jwt:jwt:method:HashBorrower.Borrow", - "internal/jwt:jwt:method:HashBorrower.ReturnAll", - "internal/jwt:jwt:method:MapClaims.Delete", - "internal/jwt:jwt:method:MapClaims.GetAccessKey", - "internal/jwt:jwt:method:MapClaims.Lookup", - "internal/jwt:jwt:method:MapClaims.Map", - "internal/jwt:jwt:method:MapClaims.MarshalJSON", - "internal/jwt:jwt:method:MapClaims.Set", - "internal/jwt:jwt:method:MapClaims.SetAccessKey", - "internal/jwt:jwt:method:MapClaims.SetExpiry", - "internal/jwt:jwt:method:MapClaims.Valid", - "internal/jwt:jwt:method:SigningMethodHMAC.HashBorrower", - "internal/jwt:jwt:method:StandardClaims.SetAccessKey", - "internal/jwt:jwt:method:StandardClaims.SetAudience", - "internal/jwt:jwt:method:StandardClaims.SetExpiry", - "internal/jwt:jwt:method:StandardClaims.SetIssuer", - "internal/jwt:jwt:method:StandardClaims.UnmarshalJSON", - "internal/jwt:jwt:method:StandardClaims.Valid", - "internal/jwt:jwt:type:HashBorrower", - "internal/jwt:jwt:type:MapClaims", - "internal/jwt:jwt:type:SigningMethodHMAC", - "internal/jwt:jwt:type:StandardClaims", - "internal/jwt:jwt:var:SigningMethodHS256", - "internal/jwt:jwt:var:SigningMethodHS384", - "internal/jwt:jwt:var:SigningMethodHS512", - "internal/kms:kms:const:Builtin", - "internal/kms:kms:const:EnvKESAPIKey", - "internal/kms:kms:const:EnvKESClientCert", - "internal/kms:kms:const:EnvKESClientKey", - "internal/kms:kms:const:EnvKESClientPassword", - "internal/kms:kms:const:EnvKESDefaultKey", - "internal/kms:kms:const:EnvKESEndpoint", - "internal/kms:kms:const:EnvKESServerCA", - "internal/kms:kms:const:EnvKMSAPIKey", - "internal/kms:kms:const:EnvKMSDefaultKey", - "internal/kms:kms:const:EnvKMSEnclave", - "internal/kms:kms:const:EnvKMSEndpoint", - "internal/kms:kms:const:EnvKMSReplicateKeyID", - "internal/kms:kms:const:EnvKMSSecretKey", - "internal/kms:kms:const:EnvKMSSecretKeyFile", - "internal/kms:kms:const:MinKES", - "internal/kms:kms:const:MinKMS", - "internal/kms:kms:field:ConnectionOptions.CADir", - "internal/kms:kms:field:CreateKeyRequest.Name", - "internal/kms:kms:field:DEK.Ciphertext", - "internal/kms:kms:field:DEK.KeyID", - "internal/kms:kms:field:DEK.Plaintext", - "internal/kms:kms:field:DEK.Version", - "internal/kms:kms:field:DecryptRequest.AssociatedData", - "internal/kms:kms:field:DecryptRequest.Ciphertext", - "internal/kms:kms:field:DecryptRequest.Name", - "internal/kms:kms:field:DecryptRequest.Version", - "internal/kms:kms:field:DeleteKeyRequest.Name", - "internal/kms:kms:field:Error.APICode", - "internal/kms:kms:field:Error.Cause", - "internal/kms:kms:field:Error.Code", - "internal/kms:kms:field:Error.Err", - "internal/kms:kms:field:GenerateKeyRequest.AssociatedData", - "internal/kms:kms:field:GenerateKeyRequest.Name", - "internal/kms:kms:field:KMS.DefaultKey", - "internal/kms:kms:field:KMS.Type", - "internal/kms:kms:field:ListRequest.ContinueAt", - "internal/kms:kms:field:ListRequest.Limit", - "internal/kms:kms:field:ListRequest.Prefix", - "internal/kms:kms:field:MACRequest.Message", - "internal/kms:kms:field:MACRequest.Name", - "internal/kms:kms:field:MACRequest.Version", - "internal/kms:kms:field:Metrics.Latency", - "internal/kms:kms:field:Metrics.ReqErr", - "internal/kms:kms:field:Metrics.ReqFail", - "internal/kms:kms:field:Metrics.ReqOK", - "internal/kms:kms:field:Status.Offline", - "internal/kms:kms:field:Status.Online", - "internal/kms:kms:field:StubKMS.KeyNames", - "internal/kms:kms:func:Connect", - "internal/kms:kms:func:IsPresent", - "internal/kms:kms:func:NewBuiltin", - "internal/kms:kms:func:NewStub", - "internal/kms:kms:func:ParseSecretKey", - "internal/kms:kms:func:ReplicateKeyID", - "internal/kms:kms:method:Context.MarshalText", - "internal/kms:kms:method:DEK.MarshalText", - "internal/kms:kms:method:DEK.UnmarshalText", - "internal/kms:kms:method:Error.Error", - "internal/kms:kms:method:KMS.APIs", - "internal/kms:kms:method:KMS.CreateKey", - "internal/kms:kms:method:KMS.Decrypt", - "internal/kms:kms:method:KMS.GenerateKey", - "internal/kms:kms:method:KMS.ListKeys", - "internal/kms:kms:method:KMS.MAC", - "internal/kms:kms:method:KMS.Metrics", - "internal/kms:kms:method:KMS.Status", - "internal/kms:kms:method:KMS.Version", - "internal/kms:kms:method:StubKMS.APIs", - "internal/kms:kms:method:StubKMS.CreateKey", - "internal/kms:kms:method:StubKMS.Decrypt", - "internal/kms:kms:method:StubKMS.GenerateKey", - "internal/kms:kms:method:StubKMS.ListKeys", - "internal/kms:kms:method:StubKMS.MAC", - "internal/kms:kms:method:StubKMS.Status", - "internal/kms:kms:method:StubKMS.Version", - "internal/kms:kms:method:Type.String", - "internal/kms:kms:method:ciphertext.UnmarshalJSON", - "internal/kms:kms:method:kesConn.APIs", - "internal/kms:kms:method:kesConn.CreateKey", - "internal/kms:kms:method:kesConn.Decrypt", - "internal/kms:kms:method:kesConn.DeleteKey", - "internal/kms:kms:method:kesConn.EncryptKey", - "internal/kms:kms:method:kesConn.GenerateKey", - "internal/kms:kms:method:kesConn.ImportKey", - "internal/kms:kms:method:kesConn.ListKeys", - "internal/kms:kms:method:kesConn.MAC", - "internal/kms:kms:method:kesConn.Status", - "internal/kms:kms:method:kesConn.Version", - "internal/kms:kms:method:kmsConn.APIs", - "internal/kms:kms:method:kmsConn.CreateKey", - "internal/kms:kms:method:kmsConn.Decrypt", - "internal/kms:kms:method:kmsConn.GenerateKey", - "internal/kms:kms:method:kmsConn.ListKeys", - "internal/kms:kms:method:kmsConn.MAC", - "internal/kms:kms:method:kmsConn.Status", - "internal/kms:kms:method:kmsConn.Version", - "internal/kms:kms:method:secretKey.APIs", - "internal/kms:kms:method:secretKey.CreateKey", - "internal/kms:kms:method:secretKey.Decrypt", - "internal/kms:kms:method:secretKey.GenerateKey", - "internal/kms:kms:method:secretKey.ListKeys", - "internal/kms:kms:method:secretKey.MAC", - "internal/kms:kms:method:secretKey.Status", - "internal/kms:kms:method:secretKey.Version", - "internal/kms:kms:type:ConnectionOptions", - "internal/kms:kms:type:Context", - "internal/kms:kms:type:CreateKeyRequest", - "internal/kms:kms:type:DEK", - "internal/kms:kms:type:DecryptRequest", - "internal/kms:kms:type:DeleteKeyRequest", - "internal/kms:kms:type:Error", - "internal/kms:kms:type:GenerateKeyRequest", - "internal/kms:kms:type:KMS", - "internal/kms:kms:type:ListRequest", - "internal/kms:kms:type:MACRequest", - "internal/kms:kms:type:Metrics", - "internal/kms:kms:type:Status", - "internal/kms:kms:type:StubKMS", - "internal/kms:kms:type:Type", - "internal/kms:kms:var:ErrDecrypt", - "internal/kms:kms:var:ErrKeyExists", - "internal/kms:kms:var:ErrKeyNotFound", - "internal/kms:kms:var:ErrNotSupported", - "internal/kms:kms:var:ErrPermission", - "internal/kms:kms:var:StubCreatedAt", - "internal/kms:kms:var:StubCreatedBy", - "internal/lock:lock:func:LockedOpenFile", - "internal/lock:lock:func:Open", - "internal/lock:lock:func:RLockedOpenFile", - "internal/lock:lock:func:TryLockedOpenFile", - "internal/lock:lock:method:RLockedFile.Close", - "internal/lock:lock:method:RLockedFile.IncLockRef", - "internal/lock:lock:method:RLockedFile.IsClosed", - "internal/lock:lock:type:LockedFile", - "internal/lock:lock:type:RLockedFile", - "internal/lock:lock:var:ErrAlreadyLocked", - "internal/logger/message/audit:audit:const:Version", - "internal/logger/message/audit:audit:func:NewEntry", - "internal/logger/message/audit:audit:func:ToEntry", - "internal/logger/target/console:console:func:New", - "internal/logger/target/console:console:method:Target.Endpoint", - "internal/logger/target/console:console:method:Target.Send", - "internal/logger/target/console:console:method:Target.String", - "internal/logger/target/console:console:method:Target.Validate", - "internal/logger/target/console:console:type:Target", - "internal/logger/target/http:http:field:Config.AuthToken", - "internal/logger/target/http:http:field:Config.BatchSize", - "internal/logger/target/http:http:field:Config.ClientCert", - "internal/logger/target/http:http:field:Config.ClientKey", - "internal/logger/target/http:http:field:Config.Enabled", - "internal/logger/target/http:http:field:Config.Endpoint", - "internal/logger/target/http:http:field:Config.HTTPTimeout", - "internal/logger/target/http:http:field:Config.LogOnceIf", - "internal/logger/target/http:http:field:Config.MaxRetry", - "internal/logger/target/http:http:field:Config.Name", - "internal/logger/target/http:http:field:Config.Proxy", - "internal/logger/target/http:http:field:Config.QueueDir", - "internal/logger/target/http:http:field:Config.QueueSize", - "internal/logger/target/http:http:field:Config.RetryIntvl", - "internal/logger/target/http:http:field:Config.Transport", - "internal/logger/target/http:http:field:Config.UserAgent", - "internal/logger/target/http:http:func:CreateOrAdjustGlobalBuffer", - "internal/logger/target/http:http:func:New", - "internal/logger/target/http:http:method:Target.AssignMigrateTarget", - "internal/logger/target/http:http:method:Target.Cancel", - "internal/logger/target/http:http:method:Target.Endpoint", - "internal/logger/target/http:http:method:Target.Init", - "internal/logger/target/http:http:method:Target.IsOnline", - "internal/logger/target/http:http:method:Target.Name", - "internal/logger/target/http:http:method:Target.Send", - "internal/logger/target/http:http:method:Target.SendFromStore", - "internal/logger/target/http:http:method:Target.Stats", - "internal/logger/target/http:http:method:Target.String", - "internal/logger/target/http:http:method:Target.Type", - "internal/logger/target/http:http:type:Config", - "internal/logger/target/http:http:type:Target", - "internal/logger/target/kafka:kafka:field:Config.Brokers", - "internal/logger/target/kafka:kafka:field:Config.Enabled", - "internal/logger/target/kafka:kafka:field:Config.LogOnce", - "internal/logger/target/kafka:kafka:field:Config.QueueDir", - "internal/logger/target/kafka:kafka:field:Config.QueueSize", - "internal/logger/target/kafka:kafka:field:Config.SASL", - "internal/logger/target/kafka:kafka:field:Config.TLS", - "internal/logger/target/kafka:kafka:field:Config.Topic", - "internal/logger/target/kafka:kafka:field:Config.Version", - "internal/logger/target/kafka:kafka:func:New", - "internal/logger/target/kafka:kafka:method:Target.Cancel", - "internal/logger/target/kafka:kafka:method:Target.Endpoint", - "internal/logger/target/kafka:kafka:method:Target.Init", - "internal/logger/target/kafka:kafka:method:Target.IsOnline", - "internal/logger/target/kafka:kafka:method:Target.Name", - "internal/logger/target/kafka:kafka:method:Target.Send", - "internal/logger/target/kafka:kafka:method:Target.SendFromStore", - "internal/logger/target/kafka:kafka:method:Target.Stats", - "internal/logger/target/kafka:kafka:method:Target.String", - "internal/logger/target/kafka:kafka:method:Target.Type", - "internal/logger/target/kafka:kafka:method:XDGSCRAMClient.Begin", - "internal/logger/target/kafka:kafka:method:XDGSCRAMClient.Done", - "internal/logger/target/kafka:kafka:method:XDGSCRAMClient.Step", - "internal/logger/target/kafka:kafka:type:Config", - "internal/logger/target/kafka:kafka:type:Target", - "internal/logger/target/kafka:kafka:type:XDGSCRAMClient", - "internal/logger/target/kafka:kafka:var:KafkaSHA256", - "internal/logger/target/kafka:kafka:var:KafkaSHA512", - "internal/logger/target/loggertypes:loggertypes:const:TargetConsole", - "internal/logger/target/loggertypes:loggertypes:const:TargetHTTP", - "internal/logger/target/loggertypes:loggertypes:const:TargetKafka", - "internal/logger/target/loggertypes:loggertypes:field:TargetStats.FailedMessages", - "internal/logger/target/loggertypes:loggertypes:field:TargetStats.QueueLength", - "internal/logger/target/loggertypes:loggertypes:field:TargetStats.TotalMessages", - "internal/logger/target/loggertypes:loggertypes:method:TargetType.String", - "internal/logger/target/loggertypes:loggertypes:type:TargetStats", - "internal/logger/target/loggertypes:loggertypes:type:TargetType", - "internal/logger/target/testlogger:testlogger:method:testLogger.Cancel", - "internal/logger/target/testlogger:testlogger:method:testLogger.Endpoint", - "internal/logger/target/testlogger:testlogger:method:testLogger.Init", - "internal/logger/target/testlogger:testlogger:method:testLogger.IsOnline", - "internal/logger/target/testlogger:testlogger:method:testLogger.Send", - "internal/logger/target/testlogger:testlogger:method:testLogger.SetErrorTB", - "internal/logger/target/testlogger:testlogger:method:testLogger.SetFatalTB", - "internal/logger/target/testlogger:testlogger:method:testLogger.SetLogTB", - "internal/logger/target/testlogger:testlogger:method:testLogger.Stats", - "internal/logger/target/testlogger:testlogger:method:testLogger.String", - "internal/logger/target/testlogger:testlogger:method:testLogger.Type", - "internal/logger/target/testlogger:testlogger:var:T", - "internal/logger:logger:const:AuthToken", - "internal/logger:logger:const:BatchSize", - "internal/logger:logger:const:ClientCert", - "internal/logger:logger:const:ClientKey", - "internal/logger:logger:const:ConsoleLoggerTgt", - "internal/logger:logger:const:Endpoint", - "internal/logger:logger:const:EnvAuditWebhookAuthToken", - "internal/logger:logger:const:EnvAuditWebhookBatchSize", - "internal/logger:logger:const:EnvAuditWebhookClientCert", - "internal/logger:logger:const:EnvAuditWebhookClientKey", - "internal/logger:logger:const:EnvAuditWebhookEnable", - "internal/logger:logger:const:EnvAuditWebhookEndpoint", - "internal/logger:logger:const:EnvAuditWebhookHTTPTimeout", - "internal/logger:logger:const:EnvAuditWebhookMaxRetry", - "internal/logger:logger:const:EnvAuditWebhookQueueDir", - "internal/logger:logger:const:EnvAuditWebhookQueueSize", - "internal/logger:logger:const:EnvAuditWebhookRetryInterval", - "internal/logger:logger:const:EnvKafkaBrokers", - "internal/logger:logger:const:EnvKafkaClientTLSCert", - "internal/logger:logger:const:EnvKafkaClientTLSKey", - "internal/logger:logger:const:EnvKafkaEnable", - "internal/logger:logger:const:EnvKafkaQueueDir", - "internal/logger:logger:const:EnvKafkaQueueSize", - "internal/logger:logger:const:EnvKafkaSASLEnable", - "internal/logger:logger:const:EnvKafkaSASLMechanism", - "internal/logger:logger:const:EnvKafkaSASLPassword", - "internal/logger:logger:const:EnvKafkaSASLUsername", - "internal/logger:logger:const:EnvKafkaTLS", - "internal/logger:logger:const:EnvKafkaTLSClientAuth", - "internal/logger:logger:const:EnvKafkaTLSSkipVerify", - "internal/logger:logger:const:EnvKafkaTopic", - "internal/logger:logger:const:EnvKafkaVersion", - "internal/logger:logger:const:EnvLoggerWebhookAuthToken", - "internal/logger:logger:const:EnvLoggerWebhookBatchSize", - "internal/logger:logger:const:EnvLoggerWebhookClientCert", - "internal/logger:logger:const:EnvLoggerWebhookClientKey", - "internal/logger:logger:const:EnvLoggerWebhookEnable", - "internal/logger:logger:const:EnvLoggerWebhookEndpoint", - "internal/logger:logger:const:EnvLoggerWebhookHTTPTimeout", - "internal/logger:logger:const:EnvLoggerWebhookMaxRetry", - "internal/logger:logger:const:EnvLoggerWebhookProxy", - "internal/logger:logger:const:EnvLoggerWebhookQueueDir", - "internal/logger:logger:const:EnvLoggerWebhookQueueSize", - "internal/logger:logger:const:EnvLoggerWebhookRetryInterval", - "internal/logger:logger:const:ErrorKind", - "internal/logger:logger:const:EventKind", - "internal/logger:logger:const:FatalKind", - "internal/logger:logger:const:InfoKind", - "internal/logger:logger:const:KafkaBrokers", - "internal/logger:logger:const:KafkaClientTLSCert", - "internal/logger:logger:const:KafkaClientTLSKey", - "internal/logger:logger:const:KafkaQueueDir", - "internal/logger:logger:const:KafkaQueueSize", - "internal/logger:logger:const:KafkaSASL", - "internal/logger:logger:const:KafkaSASLMechanism", - "internal/logger:logger:const:KafkaSASLPassword", - "internal/logger:logger:const:KafkaSASLUsername", - "internal/logger:logger:const:KafkaTLS", - "internal/logger:logger:const:KafkaTLSClientAuth", - "internal/logger:logger:const:KafkaTLSSkipVerify", - "internal/logger:logger:const:KafkaTopic", - "internal/logger:logger:const:KafkaVersion", - "internal/logger:logger:const:MaxRetry", - "internal/logger:logger:const:Proxy", - "internal/logger:logger:const:QueueDir", - "internal/logger:logger:const:QueueSize", - "internal/logger:logger:const:RetryInterval", - "internal/logger:logger:const:TimeFormat", - "internal/logger:logger:const:WarningKind", - "internal/logger:logger:field:Config.AuditKafka", - "internal/logger:logger:field:Config.AuditWebhook", - "internal/logger:logger:field:Config.Console", - "internal/logger:logger:field:Config.HTTP", - "internal/logger:logger:field:Console.Enabled", - "internal/logger:logger:field:KeyVal.Key", - "internal/logger:logger:field:KeyVal.Val", - "internal/logger:logger:field:ObjectVersion.ObjectName", - "internal/logger:logger:field:ObjectVersion.VersionID", - "internal/logger:logger:field:Options.Compress", - "internal/logger:logger:field:Options.Directory", - "internal/logger:logger:field:Options.FileNameFunc", - "internal/logger:logger:field:Options.MaximumFileSize", - "internal/logger:logger:field:ReqInfo.API", - "internal/logger:logger:field:ReqInfo.AuthType", - "internal/logger:logger:field:ReqInfo.BucketName", - "internal/logger:logger:field:ReqInfo.Cred", - "internal/logger:logger:field:ReqInfo.DeploymentID", - "internal/logger:logger:field:ReqInfo.Host", - "internal/logger:logger:field:ReqInfo.ObjectName", - "internal/logger:logger:field:ReqInfo.Objects", - "internal/logger:logger:field:ReqInfo.Owner", - "internal/logger:logger:field:ReqInfo.Region", - "internal/logger:logger:field:ReqInfo.RemoteHost", - "internal/logger:logger:field:ReqInfo.RequestID", - "internal/logger:logger:field:ReqInfo.UserAgent", - "internal/logger:logger:field:ReqInfo.VersionID", - "internal/logger:logger:field:Target.Cancel", - "internal/logger:logger:field:Target.Endpoint", - "internal/logger:logger:field:Target.Init", - "internal/logger:logger:field:Target.IsOnline", - "internal/logger:logger:field:Target.Send", - "internal/logger:logger:field:Target.Stats", - "internal/logger:logger:field:Target.String", - "internal/logger:logger:field:Target.Type", - "internal/logger:logger:func:AddSystemTarget", - "internal/logger:logger:func:AuditLog", - "internal/logger:logger:func:AuditTargets", - "internal/logger:logger:func:CriticalIf", - "internal/logger:logger:func:CurrentStats", - "internal/logger:logger:func:EnableAnonymous", - "internal/logger:logger:func:EnableJSON", - "internal/logger:logger:func:EnableQuiet", - "internal/logger:logger:func:Error", - "internal/logger:logger:func:Event", - "internal/logger:logger:func:Fatal", - "internal/logger:logger:func:FatalIf", - "internal/logger:logger:func:GetAuditEntry", - "internal/logger:logger:func:GetReqInfo", - "internal/logger:logger:func:HashString", - "internal/logger:logger:func:Info", - "internal/logger:logger:func:Init", - "internal/logger:logger:func:IsJSON", - "internal/logger:logger:func:IsQuiet", - "internal/logger:logger:func:LogAlwaysIf", - "internal/logger:logger:func:LogIf", - "internal/logger:logger:func:LogIfNot", - "internal/logger:logger:func:LogOnceConsoleIf", - "internal/logger:logger:func:LogOnceIf", - "internal/logger:logger:func:LookupConfigForSubSys", - "internal/logger:logger:func:NewConfig", - "internal/logger:logger:func:NewDir", - "internal/logger:logger:func:NewReqInfo", - "internal/logger:logger:func:RegisterError", - "internal/logger:logger:func:SetAuditEntry", - "internal/logger:logger:func:SetLoggerHTTP", - "internal/logger:logger:func:SetLoggerHTTPAudit", - "internal/logger:logger:func:SetReqInfo", - "internal/logger:logger:func:Startup", - "internal/logger:logger:func:SystemTargets", - "internal/logger:logger:func:UpdateAuditKafkaTargets", - "internal/logger:logger:func:UpdateAuditWebhooks", - "internal/logger:logger:func:UpdateHTTPWebhooks", - "internal/logger:logger:func:ValidateSubSysConfig", - "internal/logger:logger:func:Warning", - "internal/logger:logger:method:ReqInfo.AppendTags", - "internal/logger:logger:method:ReqInfo.GetTags", - "internal/logger:logger:method:ReqInfo.GetTagsMap", - "internal/logger:logger:method:ReqInfo.PopulateTagsMap", - "internal/logger:logger:method:ReqInfo.SetTags", - "internal/logger:logger:method:Writer.Close", - "internal/logger:logger:method:Writer.Write", - "internal/logger:logger:type:Config", - "internal/logger:logger:type:Console", - "internal/logger:logger:type:KeyVal", - "internal/logger:logger:type:LogOnce", - "internal/logger:logger:type:Logger", - "internal/logger:logger:type:ObjectVersion", - "internal/logger:logger:type:Options", - "internal/logger:logger:type:ReqInfo", - "internal/logger:logger:type:Target", - "internal/logger:logger:type:Writer", - "internal/logger:logger:var:DefaultAuditKafkaKVS", - "internal/logger:logger:var:DefaultAuditWebhookKVS", - "internal/logger:logger:var:DefaultLoggerWebhookKVS", - "internal/logger:logger:var:DisableLog", - "internal/logger:logger:var:ErrCritical", - "internal/logger:logger:var:ExitFunc", - "internal/logger:logger:var:Help", - "internal/logger:logger:var:HelpKafka", - "internal/logger:logger:var:HelpWebhook", - "internal/logger:logger:var:Output", - "internal/lsync:lsync:func:NewLRWMutex", - "internal/lsync:lsync:method:LRWMutex.DRLocker", - "internal/lsync:lsync:method:LRWMutex.ForceUnlock", - "internal/lsync:lsync:method:LRWMutex.GetLock", - "internal/lsync:lsync:method:LRWMutex.GetRLock", - "internal/lsync:lsync:method:LRWMutex.Lock", - "internal/lsync:lsync:method:LRWMutex.RLock", - "internal/lsync:lsync:method:LRWMutex.RUnlock", - "internal/lsync:lsync:method:LRWMutex.Unlock", - "internal/lsync:lsync:method:drlocker.Lock", - "internal/lsync:lsync:method:drlocker.Unlock", - "internal/lsync:lsync:type:LRWMutex", - "internal/mcontext:mcontext:const:ContextTraceKey", - "internal/mcontext:mcontext:field:TraceCtxt.AmzReqID", - "internal/mcontext:mcontext:field:TraceCtxt.FuncName", - "internal/mcontext:mcontext:field:TraceCtxt.RequestRecorder", - "internal/mcontext:mcontext:field:TraceCtxt.ResponseRecorder", - "internal/mcontext:mcontext:type:ContextTraceType", - "internal/mcontext:mcontext:type:TraceCtxt", - "internal/mountinfo:mountinfo:func:CheckCrossDevice", - "internal/mountinfo:mountinfo:func:IsLikelyMountPoint", - "internal/mountinfo:mountinfo:method:mountInfo.String", - "internal/net:net:func:GetInterfaceNetStats", - "internal/once:once:func:NewSingleton", - "internal/once:once:method:Init.Do", - "internal/once:once:method:Init.DoWithContext", - "internal/once:once:method:Singleton.Get", - "internal/once:once:method:Singleton.GetNonBlocking", - "internal/once:once:method:Singleton.IsSet", - "internal/once:once:method:Singleton.Set", - "internal/once:once:type:Init", - "internal/once:once:type:Singleton", - "internal/pubsub:pubsub:const:MaskAll", - "internal/pubsub:pubsub:field:Maskable.Mask", - "internal/pubsub:pubsub:func:MaskFromMaskable", - "internal/pubsub:pubsub:func:New", - "internal/pubsub:pubsub:method:Mask.Contains", - "internal/pubsub:pubsub:method:Mask.FromUint64", - "internal/pubsub:pubsub:method:Mask.Mask", - "internal/pubsub:pubsub:method:Mask.Merge", - "internal/pubsub:pubsub:method:Mask.MergeMaskable", - "internal/pubsub:pubsub:method:Mask.Overlaps", - "internal/pubsub:pubsub:method:Mask.SetIf", - "internal/pubsub:pubsub:method:Mask.SingleType", - "internal/pubsub:pubsub:method:PubSub.NumSubscribers", - "internal/pubsub:pubsub:method:PubSub.Publish", - "internal/pubsub:pubsub:method:PubSub.Subscribe", - "internal/pubsub:pubsub:method:PubSub.SubscribeJSON", - "internal/pubsub:pubsub:method:PubSub.Subscribers", - "internal/pubsub:pubsub:type:Mask", - "internal/pubsub:pubsub:type:Maskable", - "internal/pubsub:pubsub:type:PubSub", - "internal/pubsub:pubsub:type:Sub", - "internal/pubsub:pubsub:var:GetByteBuffer", - "internal/rest:rest:const:DefaultTimeout", - "internal/rest:rest:field:Client.HealthCheckFn", - "internal/rest:rest:field:Client.HealthCheckReconnectUnit", - "internal/rest:rest:field:Client.HealthCheckTimeout", - "internal/rest:rest:field:Client.MaxErrResponseSize", - "internal/rest:rest:field:Client.NoMetrics", - "internal/rest:rest:field:Client.TraceOutput", - "internal/rest:rest:field:NetworkError.Err", - "internal/rest:rest:field:RPCStats.DialAvgDuration", - "internal/rest:rest:field:RPCStats.DialErrs", - "internal/rest:rest:field:RPCStats.Errs", - "internal/rest:rest:field:RPCStats.TTFBAvgDuration", - "internal/rest:rest:func:GetRPCStats", - "internal/rest:rest:func:NewClient", - "internal/rest:rest:method:Client.Call", - "internal/rest:rest:method:Client.CallWithHTTPMethod", - "internal/rest:rest:method:Client.Close", - "internal/rest:rest:method:Client.IsOnline", - "internal/rest:rest:method:Client.LastConn", - "internal/rest:rest:method:Client.LastError", - "internal/rest:rest:method:Client.MarkOffline", - "internal/rest:rest:method:NetworkError.Error", - "internal/rest:rest:method:NetworkError.Unwrap", - "internal/rest:rest:method:respBodyMonitor.Close", - "internal/rest:rest:method:respBodyMonitor.Read", - "internal/rest:rest:method:restError.Error", - "internal/rest:rest:method:restError.Timeout", - "internal/rest:rest:type:Client", - "internal/rest:rest:type:NetworkError", - "internal/rest:rest:type:RPCStats", - "internal/rest:rest:var:ErrClientClosed", - "internal/ringbuffer:ringbuffer:func:New", - "internal/ringbuffer:ringbuffer:func:NewBuffer", - "internal/ringbuffer:ringbuffer:method:RingBuffer.Bytes", - "internal/ringbuffer:ringbuffer:method:RingBuffer.Capacity", - "internal/ringbuffer:ringbuffer:method:RingBuffer.CloseWithError", - "internal/ringbuffer:ringbuffer:method:RingBuffer.CloseWriter", - "internal/ringbuffer:ringbuffer:method:RingBuffer.Flush", - "internal/ringbuffer:ringbuffer:method:RingBuffer.Free", - "internal/ringbuffer:ringbuffer:method:RingBuffer.IsEmpty", - "internal/ringbuffer:ringbuffer:method:RingBuffer.IsFull", - "internal/ringbuffer:ringbuffer:method:RingBuffer.Length", - "internal/ringbuffer:ringbuffer:method:RingBuffer.Read", - "internal/ringbuffer:ringbuffer:method:RingBuffer.ReadByte", - "internal/ringbuffer:ringbuffer:method:RingBuffer.Reset", - "internal/ringbuffer:ringbuffer:method:RingBuffer.SetBlocking", - "internal/ringbuffer:ringbuffer:method:RingBuffer.TryRead", - "internal/ringbuffer:ringbuffer:method:RingBuffer.TryWrite", - "internal/ringbuffer:ringbuffer:method:RingBuffer.TryWriteByte", - "internal/ringbuffer:ringbuffer:method:RingBuffer.WithCancel", - "internal/ringbuffer:ringbuffer:method:RingBuffer.Write", - "internal/ringbuffer:ringbuffer:method:RingBuffer.WriteByte", - "internal/ringbuffer:ringbuffer:method:RingBuffer.WriteCloser", - "internal/ringbuffer:ringbuffer:method:RingBuffer.WriteString", - "internal/ringbuffer:ringbuffer:method:writeCloser.Close", - "internal/ringbuffer:ringbuffer:type:RingBuffer", - "internal/ringbuffer:ringbuffer:var:ErrAcquireLock", - "internal/ringbuffer:ringbuffer:var:ErrIsEmpty", - "internal/ringbuffer:ringbuffer:var:ErrIsFull", - "internal/ringbuffer:ringbuffer:var:ErrIsNotEmpty", - "internal/ringbuffer:ringbuffer:var:ErrTooMuchDataToWrite", - "internal/ringbuffer:ringbuffer:var:ErrWriteOnClosed", - "internal/s3select/csv:csv:field:ReaderArgs.AllowQuotedRecordDelimiter", - "internal/s3select/csv:csv:field:ReaderArgs.CommentCharacter", - "internal/s3select/csv:csv:field:ReaderArgs.FieldDelimiter", - "internal/s3select/csv:csv:field:ReaderArgs.FileHeaderInfo", - "internal/s3select/csv:csv:field:ReaderArgs.QuoteCharacter", - "internal/s3select/csv:csv:field:ReaderArgs.QuoteEscapeCharacter", - "internal/s3select/csv:csv:field:ReaderArgs.RecordDelimiter", - "internal/s3select/csv:csv:field:WriterArgs.FieldDelimiter", - "internal/s3select/csv:csv:field:WriterArgs.QuoteCharacter", - "internal/s3select/csv:csv:field:WriterArgs.QuoteEscapeCharacter", - "internal/s3select/csv:csv:field:WriterArgs.QuoteFields", - "internal/s3select/csv:csv:field:WriterArgs.RecordDelimiter", - "internal/s3select/csv:csv:func:NewReader", - "internal/s3select/csv:csv:func:NewRecord", - "internal/s3select/csv:csv:method:Reader.Close", - "internal/s3select/csv:csv:method:Reader.Read", - "internal/s3select/csv:csv:method:ReaderArgs.IsEmpty", - "internal/s3select/csv:csv:method:ReaderArgs.UnmarshalXML", - "internal/s3select/csv:csv:method:Record.Clone", - "internal/s3select/csv:csv:method:Record.Get", - "internal/s3select/csv:csv:method:Record.Raw", - "internal/s3select/csv:csv:method:Record.Replace", - "internal/s3select/csv:csv:method:Record.Reset", - "internal/s3select/csv:csv:method:Record.Set", - "internal/s3select/csv:csv:method:Record.WriteCSV", - "internal/s3select/csv:csv:method:Record.WriteJSON", - "internal/s3select/csv:csv:method:WriterArgs.IsEmpty", - "internal/s3select/csv:csv:method:WriterArgs.UnmarshalXML", - "internal/s3select/csv:csv:method:recordTransform.Read", - "internal/s3select/csv:csv:method:s3Error.Cause", - "internal/s3select/csv:csv:method:s3Error.Error", - "internal/s3select/csv:csv:method:s3Error.ErrorCode", - "internal/s3select/csv:csv:method:s3Error.ErrorMessage", - "internal/s3select/csv:csv:method:s3Error.HTTPStatusCode", - "internal/s3select/csv:csv:type:Reader", - "internal/s3select/csv:csv:type:ReaderArgs", - "internal/s3select/csv:csv:type:Record", - "internal/s3select/csv:csv:type:WriterArgs", - "internal/s3select/json:json:field:ReaderArgs.ContentType", - "internal/s3select/json:json:field:Record.KVS", - "internal/s3select/json:json:field:Record.SelectFormat", - "internal/s3select/json:json:field:WriterArgs.RecordDelimiter", - "internal/s3select/json:json:func:NewPReader", - "internal/s3select/json:json:func:NewReader", - "internal/s3select/json:json:func:NewRecord", - "internal/s3select/json:json:method:PReader.Close", - "internal/s3select/json:json:method:PReader.Read", - "internal/s3select/json:json:method:RawJSON.MarshalJSON", - "internal/s3select/json:json:method:Reader.Close", - "internal/s3select/json:json:method:Reader.Read", - "internal/s3select/json:json:method:ReaderArgs.IsEmpty", - "internal/s3select/json:json:method:ReaderArgs.UnmarshalXML", - "internal/s3select/json:json:method:Record.Clone", - "internal/s3select/json:json:method:Record.Get", - "internal/s3select/json:json:method:Record.Raw", - "internal/s3select/json:json:method:Record.Replace", - "internal/s3select/json:json:method:Record.Reset", - "internal/s3select/json:json:method:Record.Set", - "internal/s3select/json:json:method:Record.WriteCSV", - "internal/s3select/json:json:method:Record.WriteJSON", - "internal/s3select/json:json:method:WriterArgs.IsEmpty", - "internal/s3select/json:json:method:WriterArgs.UnmarshalXML", - "internal/s3select/json:json:method:s3Error.Cause", - "internal/s3select/json:json:method:s3Error.Error", - "internal/s3select/json:json:method:s3Error.ErrorCode", - "internal/s3select/json:json:method:s3Error.ErrorMessage", - "internal/s3select/json:json:method:s3Error.HTTPStatusCode", - "internal/s3select/json:json:method:syncReadCloser.Close", - "internal/s3select/json:json:method:syncReadCloser.Read", - "internal/s3select/json:json:type:PReader", - "internal/s3select/json:json:type:RawJSON", - "internal/s3select/json:json:type:Reader", - "internal/s3select/json:json:type:ReaderArgs", - "internal/s3select/json:json:type:Record", - "internal/s3select/json:json:type:WriterArgs", - "internal/s3select/jstream:jstream:const:Array", - "internal/s3select/jstream:jstream:const:Boolean", - "internal/s3select/jstream:jstream:const:Null", - "internal/s3select/jstream:jstream:const:Number", - "internal/s3select/jstream:jstream:const:Object", - "internal/s3select/jstream:jstream:const:String", - "internal/s3select/jstream:jstream:const:Unknown", - "internal/s3select/jstream:jstream:field:KV.Key", - "internal/s3select/jstream:jstream:field:KV.Value", - "internal/s3select/jstream:jstream:field:MetaValue.Depth", - "internal/s3select/jstream:jstream:field:MetaValue.Length", - "internal/s3select/jstream:jstream:field:MetaValue.Offset", - "internal/s3select/jstream:jstream:field:MetaValue.Value", - "internal/s3select/jstream:jstream:field:MetaValue.ValueType", - "internal/s3select/jstream:jstream:func:NewDecoder", - "internal/s3select/jstream:jstream:method:Decoder.EmitKV", - "internal/s3select/jstream:jstream:method:Decoder.Err", - "internal/s3select/jstream:jstream:method:Decoder.MaxDepth", - "internal/s3select/jstream:jstream:method:Decoder.ObjectAsKVS", - "internal/s3select/jstream:jstream:method:Decoder.Pos", - "internal/s3select/jstream:jstream:method:Decoder.Recursive", - "internal/s3select/jstream:jstream:method:Decoder.Stream", - "internal/s3select/jstream:jstream:method:DecoderError.Error", - "internal/s3select/jstream:jstream:method:DecoderError.ReaderErr", - "internal/s3select/jstream:jstream:method:KVS.MarshalJSON", - "internal/s3select/jstream:jstream:type:Decoder", - "internal/s3select/jstream:jstream:type:DecoderError", - "internal/s3select/jstream:jstream:type:KV", - "internal/s3select/jstream:jstream:type:KVS", - "internal/s3select/jstream:jstream:type:MetaValue", - "internal/s3select/jstream:jstream:type:ValueType", - "internal/s3select/jstream:jstream:var:ErrMaxDepth", - "internal/s3select/jstream:jstream:var:ErrSyntax", - "internal/s3select/jstream:jstream:var:ErrUnexpectedEOF", - "internal/s3select/parquet:parquet:func:NewParquetReader", - "internal/s3select/parquet:parquet:method:Reader.Read", - "internal/s3select/parquet:parquet:method:ReaderArgs.IsEmpty", - "internal/s3select/parquet:parquet:method:ReaderArgs.UnmarshalXML", - "internal/s3select/parquet:parquet:method:s3Error.Cause", - "internal/s3select/parquet:parquet:method:s3Error.Error", - "internal/s3select/parquet:parquet:method:s3Error.ErrorCode", - "internal/s3select/parquet:parquet:method:s3Error.ErrorMessage", - "internal/s3select/parquet:parquet:method:s3Error.HTTPStatusCode", - "internal/s3select/parquet:parquet:type:Reader", - "internal/s3select/parquet:parquet:type:ReaderArgs", - "internal/s3select/simdj:simdj:func:NewElementReader", - "internal/s3select/simdj:simdj:func:NewReader", - "internal/s3select/simdj:simdj:func:NewRecord", - "internal/s3select/simdj:simdj:method:Reader.Close", - "internal/s3select/simdj:simdj:method:Reader.Read", - "internal/s3select/simdj:simdj:method:Record.Clone", - "internal/s3select/simdj:simdj:method:Record.CloneTo", - "internal/s3select/simdj:simdj:method:Record.Get", - "internal/s3select/simdj:simdj:method:Record.Raw", - "internal/s3select/simdj:simdj:method:Record.Replace", - "internal/s3select/simdj:simdj:method:Record.Reset", - "internal/s3select/simdj:simdj:method:Record.Set", - "internal/s3select/simdj:simdj:method:Record.WriteCSV", - "internal/s3select/simdj:simdj:method:Record.WriteJSON", - "internal/s3select/simdj:simdj:method:s3Error.Cause", - "internal/s3select/simdj:simdj:method:s3Error.Error", - "internal/s3select/simdj:simdj:method:s3Error.ErrorCode", - "internal/s3select/simdj:simdj:method:s3Error.ErrorMessage", - "internal/s3select/simdj:simdj:method:s3Error.HTTPStatusCode", - "internal/s3select/simdj:simdj:method:safeCloser.Close", - "internal/s3select/simdj:simdj:method:safeCloser.Read", - "internal/s3select/simdj:simdj:type:Reader", - "internal/s3select/simdj:simdj:type:Record", - "internal/s3select/sql:sql:const:SelectFmtCSV", - "internal/s3select/sql:sql:const:SelectFmtJSON", - "internal/s3select/sql:sql:const:SelectFmtParquet", - "internal/s3select/sql:sql:const:SelectFmtSIMDJSON", - "internal/s3select/sql:sql:const:SelectFmtUnknown", - "internal/s3select/sql:sql:field:AliasedExpression.As", - "internal/s3select/sql:sql:field:AliasedExpression.Expression", - "internal/s3select/sql:sql:field:AndCondition.Condition", - "internal/s3select/sql:sql:field:Between.End", - "internal/s3select/sql:sql:field:Between.Not", - "internal/s3select/sql:sql:field:Between.Start", - "internal/s3select/sql:sql:field:CastFunc.CastType", - "internal/s3select/sql:sql:field:CastFunc.Expr", - "internal/s3select/sql:sql:field:Compare.Operand", - "internal/s3select/sql:sql:field:Compare.Operator", - "internal/s3select/sql:sql:field:Condition.Not", - "internal/s3select/sql:sql:field:Condition.Operand", - "internal/s3select/sql:sql:field:ConditionOperand.ConditionRHS", - "internal/s3select/sql:sql:field:ConditionOperand.Operand", - "internal/s3select/sql:sql:field:ConditionRHS.Between", - "internal/s3select/sql:sql:field:ConditionRHS.Compare", - "internal/s3select/sql:sql:field:ConditionRHS.In", - "internal/s3select/sql:sql:field:ConditionRHS.Like", - "internal/s3select/sql:sql:field:CountFunc.ExprArg", - "internal/s3select/sql:sql:field:CountFunc.StarArg", - "internal/s3select/sql:sql:field:DateAddFunc.DatePart", - "internal/s3select/sql:sql:field:DateAddFunc.Quantity", - "internal/s3select/sql:sql:field:DateAddFunc.Timestamp", - "internal/s3select/sql:sql:field:DateDiffFunc.DatePart", - "internal/s3select/sql:sql:field:DateDiffFunc.Timestamp1", - "internal/s3select/sql:sql:field:DateDiffFunc.Timestamp2", - "internal/s3select/sql:sql:field:Expression.And", - "internal/s3select/sql:sql:field:ExtractFunc.From", - "internal/s3select/sql:sql:field:ExtractFunc.Timeword", - "internal/s3select/sql:sql:field:FuncExpr.Cast", - "internal/s3select/sql:sql:field:FuncExpr.Count", - "internal/s3select/sql:sql:field:FuncExpr.DateAdd", - "internal/s3select/sql:sql:field:FuncExpr.DateDiff", - "internal/s3select/sql:sql:field:FuncExpr.Extract", - "internal/s3select/sql:sql:field:FuncExpr.SFunc", - "internal/s3select/sql:sql:field:FuncExpr.Substring", - "internal/s3select/sql:sql:field:FuncExpr.Trim", - "internal/s3select/sql:sql:field:Identifier.Quoted", - "internal/s3select/sql:sql:field:Identifier.Unquoted", - "internal/s3select/sql:sql:field:In.JPathExpr", - "internal/s3select/sql:sql:field:In.ListExpr", - "internal/s3select/sql:sql:field:JSONPath.BaseKey", - "internal/s3select/sql:sql:field:JSONPath.PathExpr", - "internal/s3select/sql:sql:field:JSONPathElement.ArrayWildcard", - "internal/s3select/sql:sql:field:JSONPathElement.Index", - "internal/s3select/sql:sql:field:JSONPathElement.Key", - "internal/s3select/sql:sql:field:JSONPathElement.ObjectWildcard", - "internal/s3select/sql:sql:field:Like.EscapeChar", - "internal/s3select/sql:sql:field:Like.Not", - "internal/s3select/sql:sql:field:Like.Pattern", - "internal/s3select/sql:sql:field:ListExpr.Elements", - "internal/s3select/sql:sql:field:LitValue.Boolean", - "internal/s3select/sql:sql:field:LitValue.Float", - "internal/s3select/sql:sql:field:LitValue.Int", - "internal/s3select/sql:sql:field:LitValue.Missing", - "internal/s3select/sql:sql:field:LitValue.Null", - "internal/s3select/sql:sql:field:LitValue.String", - "internal/s3select/sql:sql:field:MultOp.Left", - "internal/s3select/sql:sql:field:MultOp.Right", - "internal/s3select/sql:sql:field:NegatedTerm.Term", - "internal/s3select/sql:sql:field:ObjectKey.ID", - "internal/s3select/sql:sql:field:ObjectKey.Lit", - "internal/s3select/sql:sql:field:OpFactor.Op", - "internal/s3select/sql:sql:field:OpFactor.Right", - "internal/s3select/sql:sql:field:OpUnaryTerm.Op", - "internal/s3select/sql:sql:field:OpUnaryTerm.Right", - "internal/s3select/sql:sql:field:Operand.Left", - "internal/s3select/sql:sql:field:Operand.Right", - "internal/s3select/sql:sql:field:PrimaryTerm.FuncCall", - "internal/s3select/sql:sql:field:PrimaryTerm.JPathExpr", - "internal/s3select/sql:sql:field:PrimaryTerm.ListExpr", - "internal/s3select/sql:sql:field:PrimaryTerm.SubExpression", - "internal/s3select/sql:sql:field:PrimaryTerm.Value", - "internal/s3select/sql:sql:field:Record.Clone", - "internal/s3select/sql:sql:field:Record.Get", - "internal/s3select/sql:sql:field:Record.Raw", - "internal/s3select/sql:sql:field:Record.Replace", - "internal/s3select/sql:sql:field:Record.Reset", - "internal/s3select/sql:sql:field:Record.Set", - "internal/s3select/sql:sql:field:Record.WriteCSV", - "internal/s3select/sql:sql:field:Record.WriteJSON", - "internal/s3select/sql:sql:field:Select.Expression", - "internal/s3select/sql:sql:field:Select.From", - "internal/s3select/sql:sql:field:Select.Limit", - "internal/s3select/sql:sql:field:Select.Where", - "internal/s3select/sql:sql:field:SelectExpression.All", - "internal/s3select/sql:sql:field:SelectExpression.Expressions", - "internal/s3select/sql:sql:field:SimpleArgFunc.ArgsList", - "internal/s3select/sql:sql:field:SimpleArgFunc.FunctionName", - "internal/s3select/sql:sql:field:SubstringFunc.Arg2", - "internal/s3select/sql:sql:field:SubstringFunc.Arg3", - "internal/s3select/sql:sql:field:SubstringFunc.Expr", - "internal/s3select/sql:sql:field:SubstringFunc.For", - "internal/s3select/sql:sql:field:SubstringFunc.From", - "internal/s3select/sql:sql:field:TableExpression.As", - "internal/s3select/sql:sql:field:TableExpression.Table", - "internal/s3select/sql:sql:field:TrimFunc.TrimChars", - "internal/s3select/sql:sql:field:TrimFunc.TrimFrom", - "internal/s3select/sql:sql:field:TrimFunc.TrimWhere", - "internal/s3select/sql:sql:field:UnaryTerm.Negated", - "internal/s3select/sql:sql:field:UnaryTerm.Primary", - "internal/s3select/sql:sql:field:WriteCSVOpts.AlwaysQuote", - "internal/s3select/sql:sql:field:WriteCSVOpts.FieldDelimiter", - "internal/s3select/sql:sql:field:WriteCSVOpts.Quote", - "internal/s3select/sql:sql:field:WriteCSVOpts.QuoteEscape", - "internal/s3select/sql:sql:func:FormatSQLTimestamp", - "internal/s3select/sql:sql:func:FromArray", - "internal/s3select/sql:sql:func:FromBool", - "internal/s3select/sql:sql:func:FromBytes", - "internal/s3select/sql:sql:func:FromFloat", - "internal/s3select/sql:sql:func:FromInt", - "internal/s3select/sql:sql:func:FromMissing", - "internal/s3select/sql:sql:func:FromNull", - "internal/s3select/sql:sql:func:FromString", - "internal/s3select/sql:sql:func:FromTimestamp", - "internal/s3select/sql:sql:func:IterToValue", - "internal/s3select/sql:sql:func:ParseSelectStatement", - "internal/s3select/sql:sql:method:Boolean.Capture", - "internal/s3select/sql:sql:method:Identifier.String", - "internal/s3select/sql:sql:method:JSONPath.String", - "internal/s3select/sql:sql:method:JSONPath.StripTableAlias", - "internal/s3select/sql:sql:method:JSONPathElement.String", - "internal/s3select/sql:sql:method:LiteralList.Capture", - "internal/s3select/sql:sql:method:LiteralString.Capture", - "internal/s3select/sql:sql:method:ObjectKey.String", - "internal/s3select/sql:sql:method:QuotedIdentifier.Capture", - "internal/s3select/sql:sql:method:SelectStatement.AggregateResult", - "internal/s3select/sql:sql:method:SelectStatement.AggregateRow", - "internal/s3select/sql:sql:method:SelectStatement.Eval", - "internal/s3select/sql:sql:method:SelectStatement.EvalFrom", - "internal/s3select/sql:sql:method:SelectStatement.IsAggregated", - "internal/s3select/sql:sql:method:SelectStatement.LimitReached", - "internal/s3select/sql:sql:method:TableExpression.HasKeypath", - "internal/s3select/sql:sql:method:Value.CSVString", - "internal/s3select/sql:sql:method:Value.Equals", - "internal/s3select/sql:sql:method:Value.GetTypeString", - "internal/s3select/sql:sql:method:Value.InferBytesType", - "internal/s3select/sql:sql:method:Value.IsArray", - "internal/s3select/sql:sql:method:Value.IsMissing", - "internal/s3select/sql:sql:method:Value.IsNull", - "internal/s3select/sql:sql:method:Value.MarshalJSON", - "internal/s3select/sql:sql:method:Value.Repr", - "internal/s3select/sql:sql:method:Value.SameTypeAs", - "internal/s3select/sql:sql:method:Value.String", - "internal/s3select/sql:sql:method:Value.ToArray", - "internal/s3select/sql:sql:method:Value.ToBool", - "internal/s3select/sql:sql:method:Value.ToBytes", - "internal/s3select/sql:sql:method:Value.ToFloat", - "internal/s3select/sql:sql:method:Value.ToInt", - "internal/s3select/sql:sql:method:Value.ToString", - "internal/s3select/sql:sql:method:Value.ToTimestamp", - "internal/s3select/sql:sql:method:s3Error.Cause", - "internal/s3select/sql:sql:method:s3Error.Error", - "internal/s3select/sql:sql:method:s3Error.ErrorCode", - "internal/s3select/sql:sql:method:s3Error.ErrorMessage", - "internal/s3select/sql:sql:method:s3Error.HTTPStatusCode", - "internal/s3select/sql:sql:type:AliasedExpression", - "internal/s3select/sql:sql:type:AndCondition", - "internal/s3select/sql:sql:type:Between", - "internal/s3select/sql:sql:type:Boolean", - "internal/s3select/sql:sql:type:CastFunc", - "internal/s3select/sql:sql:type:Compare", - "internal/s3select/sql:sql:type:Condition", - "internal/s3select/sql:sql:type:ConditionOperand", - "internal/s3select/sql:sql:type:ConditionRHS", - "internal/s3select/sql:sql:type:CountFunc", - "internal/s3select/sql:sql:type:DateAddFunc", - "internal/s3select/sql:sql:type:DateDiffFunc", - "internal/s3select/sql:sql:type:Expression", - "internal/s3select/sql:sql:type:ExtractFunc", - "internal/s3select/sql:sql:type:FuncExpr", - "internal/s3select/sql:sql:type:FuncName", - "internal/s3select/sql:sql:type:Identifier", - "internal/s3select/sql:sql:type:In", - "internal/s3select/sql:sql:type:JSONPath", - "internal/s3select/sql:sql:type:JSONPathElement", - "internal/s3select/sql:sql:type:Like", - "internal/s3select/sql:sql:type:ListExpr", - "internal/s3select/sql:sql:type:LitValue", - "internal/s3select/sql:sql:type:LiteralList", - "internal/s3select/sql:sql:type:LiteralString", - "internal/s3select/sql:sql:type:Missing", - "internal/s3select/sql:sql:type:MultOp", - "internal/s3select/sql:sql:type:NegatedTerm", - "internal/s3select/sql:sql:type:ObjectKey", - "internal/s3select/sql:sql:type:OpFactor", - "internal/s3select/sql:sql:type:OpUnaryTerm", - "internal/s3select/sql:sql:type:Operand", - "internal/s3select/sql:sql:type:PrimaryTerm", - "internal/s3select/sql:sql:type:QuotedIdentifier", - "internal/s3select/sql:sql:type:Record", - "internal/s3select/sql:sql:type:Select", - "internal/s3select/sql:sql:type:SelectExpression", - "internal/s3select/sql:sql:type:SelectObjectFormat", - "internal/s3select/sql:sql:type:SelectStatement", - "internal/s3select/sql:sql:type:SimpleArgFunc", - "internal/s3select/sql:sql:type:SubstringFunc", - "internal/s3select/sql:sql:type:TableExpression", - "internal/s3select/sql:sql:type:TrimFunc", - "internal/s3select/sql:sql:type:UnaryTerm", - "internal/s3select/sql:sql:type:Value", - "internal/s3select/sql:sql:type:WriteCSVOpts", - "internal/s3select/sql:sql:var:SQLParser", - "internal/s3select:s3select:field:InputSerialization.CSVArgs", - "internal/s3select:s3select:field:InputSerialization.CompressionType", - "internal/s3select:s3select:field:InputSerialization.JSONArgs", - "internal/s3select:s3select:field:InputSerialization.ParquetArgs", - "internal/s3select:s3select:field:OutputSerialization.CSVArgs", - "internal/s3select:s3select:field:OutputSerialization.JSONArgs", - "internal/s3select:s3select:field:RequestProgress.Enabled", - "internal/s3select:s3select:field:S3Select.Expression", - "internal/s3select:s3select:field:S3Select.ExpressionType", - "internal/s3select:s3select:field:S3Select.Input", - "internal/s3select:s3select:field:S3Select.Output", - "internal/s3select:s3select:field:S3Select.Progress", - "internal/s3select:s3select:field:S3Select.ScanRange", - "internal/s3select:s3select:field:S3Select.XMLName", - "internal/s3select:s3select:field:ScanRange.End", - "internal/s3select:s3select:field:ScanRange.Start", - "internal/s3select:s3select:field:SelectError.Cause", - "internal/s3select:s3select:field:SelectError.Error", - "internal/s3select:s3select:field:SelectError.ErrorCode", - "internal/s3select:s3select:field:SelectError.ErrorMessage", - "internal/s3select:s3select:field:SelectError.HTTPStatusCode", - "internal/s3select:s3select:func:NewErrorMessage", - "internal/s3select:s3select:func:NewObjectReadSeekCloser", - "internal/s3select:s3select:func:NewS3Select", - "internal/s3select:s3select:method:CompressionType.UnmarshalXML", - "internal/s3select:s3select:method:InputSerialization.IsEmpty", - "internal/s3select:s3select:method:InputSerialization.UnmarshalXML", - "internal/s3select:s3select:method:ObjectReadSeekCloser.Close", - "internal/s3select:s3select:method:ObjectReadSeekCloser.Read", - "internal/s3select:s3select:method:ObjectReadSeekCloser.Seek", - "internal/s3select:s3select:method:OutputSerialization.IsEmpty", - "internal/s3select:s3select:method:OutputSerialization.UnmarshalXML", - "internal/s3select:s3select:method:S3Select.Close", - "internal/s3select:s3select:method:S3Select.Evaluate", - "internal/s3select:s3select:method:S3Select.Open", - "internal/s3select:s3select:method:S3Select.UnmarshalXML", - "internal/s3select:s3select:method:ScanRange.StartLen", - "internal/s3select:s3select:method:ScanRange.Validate", - "internal/s3select:s3select:method:countUpReader.BytesRead", - "internal/s3select:s3select:method:countUpReader.Read", - "internal/s3select:s3select:method:messageWriter.Finish", - "internal/s3select:s3select:method:messageWriter.FinishWithError", - "internal/s3select:s3select:method:messageWriter.SendRecord", - "internal/s3select:s3select:method:nopReadCloser.Close", - "internal/s3select:s3select:method:nopReadCloser.Read", - "internal/s3select:s3select:method:progressReader.Close", - "internal/s3select:s3select:method:progressReader.Read", - "internal/s3select:s3select:method:progressReader.Stats", - "internal/s3select:s3select:method:s3Error.Cause", - "internal/s3select:s3select:method:s3Error.Error", - "internal/s3select:s3select:method:s3Error.ErrorCode", - "internal/s3select:s3select:method:s3Error.ErrorMessage", - "internal/s3select:s3select:method:s3Error.HTTPStatusCode", - "internal/s3select:s3select:type:CompressionType", - "internal/s3select:s3select:type:InputSerialization", - "internal/s3select:s3select:type:ObjectReadSeekCloser", - "internal/s3select:s3select:type:ObjectSegmentReaderFn", - "internal/s3select:s3select:type:OutputSerialization", - "internal/s3select:s3select:type:RequestProgress", - "internal/s3select:s3select:type:S3Select", - "internal/s3select:s3select:type:ScanRange", - "internal/s3select:s3select:type:SelectError", - "internal/store:store:field:BatchConfig.CommitTimeout", - "internal/store:store:field:BatchConfig.Limit", - "internal/store:store:field:BatchConfig.Log", - "internal/store:store:field:BatchConfig.Store", - "internal/store:store:field:Key.Compress", - "internal/store:store:field:Key.Extension", - "internal/store:store:field:Key.ItemCount", - "internal/store:store:field:Key.Name", - "internal/store:store:field:Store.Del", - "internal/store:store:field:Store.Delete", - "internal/store:store:field:Store.Get", - "internal/store:store:field:Store.GetMultiple", - "internal/store:store:field:Store.GetRaw", - "internal/store:store:field:Store.Len", - "internal/store:store:field:Store.List", - "internal/store:store:field:Store.Open", - "internal/store:store:field:Store.Put", - "internal/store:store:field:Store.PutMultiple", - "internal/store:store:field:Store.PutRaw", - "internal/store:store:field:Target.Name", - "internal/store:store:field:Target.SendFromStore", - "internal/store:store:func:NewBatch", - "internal/store:store:func:NewQueueStore", - "internal/store:store:func:StreamItems", - "internal/store:store:method:Batch.Add", - "internal/store:store:method:Batch.Close", - "internal/store:store:method:Batch.Len", - "internal/store:store:method:Key.String", - "internal/store:store:method:QueueStore.Del", - "internal/store:store:method:QueueStore.Delete", - "internal/store:store:method:QueueStore.Get", - "internal/store:store:method:QueueStore.GetMultiple", - "internal/store:store:method:QueueStore.GetRaw", - "internal/store:store:method:QueueStore.Len", - "internal/store:store:method:QueueStore.List", - "internal/store:store:method:QueueStore.Open", - "internal/store:store:method:QueueStore.Put", - "internal/store:store:method:QueueStore.PutMultiple", - "internal/store:store:method:QueueStore.PutRaw", - "internal/store:store:type:Batch", - "internal/store:store:type:BatchConfig", - "internal/store:store:type:Key", - "internal/store:store:type:QueueStore", - "internal/store:store:type:Store", - "internal/store:store:type:Target", - "internal/store:store:var:ErrBatchFull", - "internal/store:store:var:ErrNotConnected" - ], "brand_allowlist": [ "cmd/admin-bucket-handlers.go=\"MinIO admin API\"", "cmd/admin-handlers.go=\"MinIO admin API\"", @@ -10217,9 +1062,10 @@ "cmd/object-handlers.go=\"minio-federated\"", "cmd/object-handlers.go=\"minio.metadata.\"", "cmd/object-handlers.go=\"minio.versionId\"", - "cmd/object-multipart-handlers.go=\"X-Minio-Replication-Server-Side-Encryption-Iv\"", - "cmd/object-multipart-handlers.go=\"X-Minio-Replication-Server-Side-Encryption-Seal-Algorithm\"", - "cmd/object-multipart-handlers.go=\"X-Minio-Replication-Server-Side-Encryption-Sealed-Key\"", + "cmd/replication-trust.go=\"X-Minio-Replication-Encrypted-Multipart\"", + "cmd/replication-trust.go=\"X-Minio-Replication-Server-Side-Encryption-Iv\"", + "cmd/replication-trust.go=\"X-Minio-Replication-Server-Side-Encryption-Seal-Algorithm\"", + "cmd/replication-trust.go=\"X-Minio-Replication-Server-Side-Encryption-Sealed-Key\"", "cmd/s3-zip-handlers.go=\"x-minio-extract\"", "cmd/server-startup-msg.go=\"https://silo.pgsty.com/reference/minio-mc/#quickstart\"", "cmd/storage-rest-server.go=\"X-Minio-Time\"", diff --git a/buildscripts/rebrand-guard/main.go b/buildscripts/rebrand-guard/main.go index 9546c412e..261ec0446 100644 --- a/buildscripts/rebrand-guard/main.go +++ b/buildscripts/rebrand-guard/main.go @@ -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 { diff --git a/buildscripts/resolve-right-versions.sh b/buildscripts/resolve-right-versions.sh index 3b5a1a3f3..31daf77f2 100755 --- a/buildscripts/resolve-right-versions.sh +++ b/buildscripts/resolve-right-versions.sh @@ -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 diff --git a/buildscripts/rewrite-old-new.sh b/buildscripts/rewrite-old-new.sh index 7dda4db9c..2b9a3f5bd 100755 --- a/buildscripts/rewrite-old-new.sh +++ b/buildscripts/rewrite-old-new.sh @@ -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" diff --git a/buildscripts/test-timeout.sh b/buildscripts/test-timeout.sh index e27b79087..bc28bd36d 100644 --- a/buildscripts/test-timeout.sh +++ b/buildscripts/test-timeout.sh @@ -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} diff --git a/buildscripts/verify-build.sh b/buildscripts/verify-build.sh index ad2515e1a..b2e6c05ec 100755 --- a/buildscripts/verify-build.sh +++ b/buildscripts/verify-build.sh @@ -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 \ @@ -282,7 +282,6 @@ function main() { purge "$WORK_DIR" } -(__init__ "$@" && main "$@") -rv=$? -purge "$WORK_DIR" -exit "$rv" +trap 'purge "$WORK_DIR"' EXIT +__init__ "$@" +main "$@" diff --git a/buildscripts/verify-healing-empty-erasure-set.sh b/buildscripts/verify-healing-empty-erasure-set.sh index 92acebd1c..a2903e11f 100755 --- a/buildscripts/verify-healing-empty-erasure-set.sh +++ b/buildscripts/verify-healing-empty-erasure-set.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 diff --git a/buildscripts/verify-healing-with-root-disks.sh b/buildscripts/verify-healing-with-root-disks.sh index f3f36cd3c..a102b2e91 100755 --- a/buildscripts/verify-healing-with-root-disks.sh +++ b/buildscripts/verify-healing-with-root-disks.sh @@ -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 diff --git a/buildscripts/verify-healing.sh b/buildscripts/verify-healing.sh index 33bd68245..48ced08d8 100755 --- a/buildscripts/verify-healing.sh +++ b/buildscripts/verify-healing.sh @@ -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 diff --git a/buildscripts/verify-helm-migration.sh b/buildscripts/verify-helm-migration.sh index f044ecaba..de33723c9 100755 --- a/buildscripts/verify-helm-migration.sh +++ b/buildscripts/verify-helm-migration.sh @@ -113,7 +113,7 @@ helm_run template my-release "${new_chart}" \ go run ./buildscripts/helm-migration-guard "${old_render}" "${new_render}" helm_run package "${new_chart}" --destination "${output_dir}" >/dev/null -test -s "${work_dir}/silo-7.0.1.tgz" +test -s "${work_dir}/silo-7.0.2.tgz" if find "${work_dir}" -maxdepth 1 -type f -name 'minio-*.tgz' | grep -q .; then echo "Helm packaging emitted a legacy MinIO chart name" >&2 exit 1 diff --git a/buildscripts/verify-rebrand.sh b/buildscripts/verify-rebrand.sh index 91e9e7c2d..47c85104f 100755 --- a/buildscripts/verify-rebrand.sh +++ b/buildscripts/verify-rebrand.sh @@ -159,11 +159,13 @@ fi # The repository and its default branch are pgsty/silo and main. The invariant # is that the old name is never a live target, not that it is never spoken: the # READMEs have to name it to explain the rename and to point at the archived -# artifacts, which is the opposite of stranding a reader on it. +# artifacts, which is the opposite of stranding a reader on it. CONTRIBUTORS.md +# also quotes historical issue titles. # # So two rules. First, no live URL may resolve to the old repository anywhere, -# READMEs included. -stale_repo_url="$(rg -n -e 'github\.com/pgsty/minio' -e 'hub\.docker\.com/r/pgsty/minio' \ +# READMEs and CONTRIBUTORS.md included. +old_repo_pattern='pgsty/minio(\.git)?([^[:alnum:]_.-]|$)' +stale_repo_url="$(rg -n -e "github\.com/${old_repo_pattern}" -e "hub\.docker\.com/r/${old_repo_pattern}" \ --glob '!.git/**' --glob '!dist/**' \ --glob '!SILO_REBRANDING_MIGRATION.md' \ --glob '!buildscripts/rebrand-guard/compat-baseline.json' . | @@ -175,10 +177,10 @@ fi # Second, the bare name may only appear where it is deliberate: the pinned # pre-rebrand image digest in the upgrade test, the two guards that refuse a -# legacy image, and the two READMEs that document the rename and the archived -# minio branch. -repo_guard_allowlist='^(buildscripts/minio-upgrade\.sh|buildscripts/verify-rebrand\.sh|buildscripts/helm-migration-guard/main\.go|README\.md|README_ZH\.md):' -stale_repo="$(rg -n 'pgsty/minio' --glob '!.git/**' --glob '!dist/**' \ +# legacy image, the two READMEs that document the rename and the archived +# minio branch, and historical issue titles in CONTRIBUTORS.md. +repo_guard_allowlist='^(buildscripts/minio-upgrade\.sh|buildscripts/verify-rebrand\.sh|buildscripts/helm-migration-guard/main\.go|README\.md|README_ZH\.md|CONTRIBUTORS\.md):' +stale_repo="$(rg -n "${old_repo_pattern}" --glob '!.git/**' --glob '!dist/**' \ --glob '!SILO_REBRANDING_MIGRATION.md' \ --glob '!buildscripts/rebrand-guard/compat-baseline.json' . | sed 's#^\./##' | grep -Ev "${repo_guard_allowlist}" || true)" diff --git a/cmd/acl-handlers.go b/cmd/acl-handlers.go index eb1f3c1ea..999715650 100644 --- a/cmd/acl-handlers.go +++ b/cmd/acl-handlers.go @@ -25,7 +25,7 @@ import ( xhttp "github.com/minio/minio/internal/http" "github.com/minio/minio/internal/logger" "github.com/minio/mux" - "github.com/minio/pkg/v3/policy" + "github.com/pgsty/silo-pkg/v3/policy" ) // Data types used for returning dummy access control diff --git a/cmd/admin-bucket-cors-roundtrip_test.go b/cmd/admin-bucket-cors-roundtrip_test.go new file mode 100644 index 000000000..dc8e512d0 --- /dev/null +++ b/cmd/admin-bucket-cors-roundtrip_test.go @@ -0,0 +1,348 @@ +package cmd + +import ( + "archive/zip" + "bytes" + "encoding/base64" + "encoding/json" + "io" + "net/http" + "net/http/httptest" + "strings" + "testing" + "time" + + "github.com/minio/madmin-go/v3" + "github.com/minio/minio/internal/auth" + "github.com/minio/mux" +) + +func corsAdminRequest(t *testing.T, cred auth.Credentials, method, path string, body []byte) *httptest.ResponseRecorder { + t.Helper() + router := mux.NewRouter() + registerAdminRouter(router, true) + req, err := newTestSignedRequestV4(method, adminPathPrefix+adminAPIVersionPrefix+path, + int64(len(body)), bytes.NewReader(body), cred.AccessKey, cred.SecretKey, nil) + if err != nil { + t.Fatal(err) + } + rec := httptest.NewRecorder() + router.ServeHTTP(rec, req) + if rec.Code != http.StatusOK { + t.Fatalf("admin %s: %d: %s", path, rec.Code, rec.Body.String()) + } + return rec +} + +func corsImportReport(t *testing.T, rec *httptest.ResponseRecorder) madmin.BucketMetaImportErrs { + t.Helper() + var rpt madmin.BucketMetaImportErrs + if err := json.Unmarshal(rec.Body.Bytes(), &rpt); err != nil { + t.Fatalf("import report %q: %v", rec.Body.String(), err) + } + return rpt +} + +func corsZip(t *testing.T, entries map[string][]byte) []byte { + t.Helper() + var buf bytes.Buffer + zw := zip.NewWriter(&buf) + for name, data := range entries { + w, err := zw.Create(name) + if err != nil { + t.Fatal(err) + } + if _, err = w.Write(data); err != nil { + t.Fatal(err) + } + } + if err := zw.Close(); err != nil { + t.Fatal(err) + } + return buf.Bytes() +} + +// corsCorruptedZip builds an archive holding a stored (uncompressed) cors.xml +// whose payload is altered after the checksum is computed, plus the given +// companion entries. The altered document stays well formed, so only the zip +// checksum tells the two apart. +func corsCorruptedZip(t *testing.T, name string, doc []byte, others map[string][]byte) []byte { + t.Helper() + var buf bytes.Buffer + zw := zip.NewWriter(&buf) + w, err := zw.CreateHeader(&zip.FileHeader{Name: name, Method: zip.Store}) + if err != nil { + t.Fatal(err) + } + if _, err = w.Write(doc); err != nil { + t.Fatal(err) + } + for other, data := range others { + ow, err := zw.Create(other) + if err != nil { + t.Fatal(err) + } + if _, err = ow.Write(data); err != nil { + t.Fatal(err) + } + } + if err = zw.Close(); err != nil { + t.Fatal(err) + } + raw := buf.Bytes() + at := bytes.Index(raw, []byte("app.example.com")) + if at < 0 { + t.Fatalf("stored CORS payload not found in archive") + } + raw[at] = 'A' + return raw +} + +// TestAdminBucketMetadataCORSRoundTrip covers the export/import round trip for +// per-bucket CORS, per-file error reporting for an invalid document, and that +// an archive without cors.xml leaves an existing configuration alone. +func TestAdminBucketMetadataCORSRoundTrip(t *testing.T) { + ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{t: t, objAPITest: func(obj ObjectLayer, instanceType, bucket string, _ http.Handler, cred auth.Credentials, t *testing.T) { + corsXML := []byte(testSiteReplicationCORSDoc) + if _, err := updateLocalBucketCORSMetadata(t.Context(), obj, bucket, corsXML); err != nil { + t.Fatal(err) + } + + // Export must carry the stored document verbatim. + rec := corsAdminRequest(t, cred, http.MethodGet, "/export-bucket-metadata?bucket="+bucket, nil) + archive := rec.Body.Bytes() + zr, err := zip.NewReader(bytes.NewReader(archive), int64(len(archive))) + if err != nil { + t.Fatal(err) + } + var exported []byte + for _, f := range zr.File { + if f.Name != bucket+"/"+bucketCorsConfig { + continue + } + r, err := f.Open() + if err != nil { + t.Fatal(err) + } + exported, err = io.ReadAll(r) + r.Close() + if err != nil { + t.Fatal(err) + } + } + if !bytes.Equal(exported, corsXML) { + t.Fatalf("%s: exported CORS = %q, want %q", instanceType, exported, corsXML) + } + + // Drop the configuration: the archive must then omit the entry. + if _, err = updateLocalBucketCORSMetadata(t.Context(), obj, bucket, nil); err != nil { + t.Fatal(err) + } + if _, _, err = globalBucketMetadataSys.GetCorsConfigXML(bucket); err == nil { + t.Fatalf("%s: CORS still present before restore", instanceType) + } + rec = corsAdminRequest(t, cred, http.MethodGet, "/export-bucket-metadata?bucket="+bucket, nil) + empty := rec.Body.Bytes() + zr, err = zip.NewReader(bytes.NewReader(empty), int64(len(empty))) + if err != nil { + t.Fatal(err) + } + for _, f := range zr.File { + if f.Name == bucket+"/"+bucketCorsConfig { + t.Fatalf("%s: export emitted %s for a bucket without CORS", instanceType, f.Name) + } + } + + rec = corsAdminRequest(t, cred, http.MethodPut, "/import-bucket-metadata", archive) + if st := corsImportReport(t, rec).Buckets[bucket]; !st.Cors.IsSet || st.Cors.Err != "" { + t.Fatalf("%s: import report cors = %+v", instanceType, st.Cors) + } + stored, storedAt, err := globalBucketMetadataSys.GetCorsConfigXML(bucket) + if err != nil || !bytes.Equal(stored, corsXML) { + t.Fatalf("%s: restored CORS = %q, err = %v", instanceType, stored, err) + } + created, err := globalBucketMetadataSys.CreatedAt(bucket) + if err != nil { + t.Fatal(err) + } + if !storedAt.After(created) { + t.Fatalf("%s: restored CORS timestamp %v is not after bucket creation %v", instanceType, storedAt, created) + } + + // An archive without cors.xml must not remove the configuration. + corsAdminRequest(t, cred, http.MethodPut, "/import-bucket-metadata", + corsZip(t, map[string][]byte{bucket + "/quota.json": []byte(`{"quota":0}`)})) + if stored, _, err = globalBucketMetadataSys.GetCorsConfigXML(bucket); err != nil || !bytes.Equal(stored, corsXML) { + t.Fatalf("%s: import without cors.xml changed CORS: %q, err = %v", instanceType, stored, err) + } + + // A bucket the import itself creates must still land above its own + // creation time, otherwise CORS replication would drop the restore. + fresh := "cors-import-created-bucket" + rec = corsAdminRequest(t, cred, http.MethodPut, "/import-bucket-metadata", + corsZip(t, map[string][]byte{fresh + "/" + bucketCorsConfig: corsXML})) + if st := corsImportReport(t, rec).Buckets[fresh]; !st.Cors.IsSet || st.Cors.Err != "" { + t.Fatalf("%s: fresh bucket import report cors = %+v", instanceType, st.Cors) + } + freshStored, freshAt, err := globalBucketMetadataSys.GetCorsConfigXML(fresh) + if err != nil || !bytes.Equal(freshStored, corsXML) { + t.Fatalf("%s: fresh bucket CORS = %q, err = %v", instanceType, freshStored, err) + } + freshCreated, err := globalBucketMetadataSys.CreatedAt(fresh) + if err != nil { + t.Fatal(err) + } + if !freshAt.After(freshCreated) { + t.Fatalf("%s: fresh bucket CORS timestamp %v is not after creation %v", instanceType, freshAt, freshCreated) + } + + // An invalid document must fail loudly for that bucket and change nothing. + rec = corsAdminRequest(t, cred, http.MethodPut, "/import-bucket-metadata", + corsZip(t, map[string][]byte{bucket + "/" + bucketCorsConfig: []byte("")})) + if st := corsImportReport(t, rec).Buckets[bucket]; st.Cors.Err == "" { + t.Fatalf("%s: invalid CORS import reported no error: %+v", instanceType, st) + } + if stored, _, err = globalBucketMetadataSys.GetCorsConfigXML(bucket); err != nil || !bytes.Equal(stored, corsXML) { + t.Fatalf("%s: invalid CORS import changed stored config: %q, err = %v", instanceType, stored, err) + } + + // A well formed document carried by a corrupt zip entry must be + // rejected too, leaving the stored document and its timestamp alone + // while the other configs in the same archive still apply. + _, corsAt, err := globalBucketMetadataSys.GetCorsConfigXML(bucket) + if err != nil { + t.Fatal(err) + } + rec = corsAdminRequest(t, cred, http.MethodPut, "/import-bucket-metadata", + corsCorruptedZip(t, bucket+"/"+bucketCorsConfig, corsXML, + map[string][]byte{bucket + "/quota.json": []byte(`{"quota":4096,"quotatype":"hard"}`)})) + st := corsImportReport(t, rec).Buckets[bucket] + if st.Cors.Err == "" { + t.Fatalf("%s: corrupt CORS entry reported no error: %+v", instanceType, st) + } + if !st.Quota.IsSet || st.Quota.Err != "" { + t.Fatalf("%s: corrupt CORS entry blocked the neighboring quota: %+v", instanceType, st.Quota) + } + stored, storedAt, err = globalBucketMetadataSys.GetCorsConfigXML(bucket) + if err != nil || !bytes.Equal(stored, corsXML) || !storedAt.Equal(corsAt) { + t.Fatalf("%s: corrupt CORS entry changed stored config: %q at %v (was %v), err = %v", instanceType, stored, storedAt, corsAt, err) + } + quota, _, err := globalBucketMetadataSys.GetQuotaConfig(t.Context(), bucket) + if err != nil || quota == nil || quota.Quota != 4096 { + t.Fatalf("%s: neighboring quota not applied: %+v, err = %v", instanceType, quota, err) + } + }}) +} + +// corsPeerStub is a stand-in site-replication peer. It records every +// SRBucketMeta it is asked to apply and answers with status. +func corsPeerStub(t *testing.T, applied chan<- madmin.SRBucketMeta, status int) *httptest.Server { + t.Helper() + return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method == http.MethodPut && applied != nil { + var item madmin.SRBucketMeta + if err := json.NewDecoder(r.Body).Decode(&item); err != nil { + t.Errorf("decode peer apply: %v", err) + w.WriteHeader(http.StatusBadRequest) + return + } + applied <- item + } + w.WriteHeader(status) + })) +} + +// TestAdminBucketMetadataCORSImportReplicatesPastPeerFailure pins that an +// imported CORS document reaches the reachable peers even when the shared +// bucket metadata hook failed against an unreachable one, and that both +// failures are still reported for the bucket. +func TestAdminBucketMetadataCORSImportReplicatesPastPeerFailure(t *testing.T) { + ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{t: t, objAPITest: func(obj ObjectLayer, instanceType, bucket string, _ http.Handler, cred auth.Credentials, t *testing.T) { + ctx := t.Context() + corsXML := []byte(testSiteReplicationCORSDoc) + + healthyApplies := make(chan madmin.SRBucketMeta, 4) + healthy := corsPeerStub(t, healthyApplies, http.StatusOK) + defer healthy.Close() + broken := corsPeerStub(t, nil, http.StatusBadRequest) + defer broken.Close() + + // With site replication on, admin requests resolve their token signing + // key through the site replicator account, so it has to exist. + serviceCred, err := auth.CreateCredentials(siteReplicatorSvcAcc, "cors-import-service-secret") + if err != nil { + t.Fatal(err) + } + serviceCred.ParentUser = cred.AccessKey + if _, err = globalIAMSys.store.AddServiceAccount(ctx, serviceCred); err != nil { + t.Fatal(err) + } + defer globalIAMSys.DeleteServiceAccount(ctx, serviceCred.AccessKey, false) + globalSiteReplicatorCred.Set(serviceCred.SecretKey) + defer globalSiteReplicatorCred.Set("") + + globalSiteReplicationSys.Lock() + oldEnabled, oldState := globalSiteReplicationSys.enabled, globalSiteReplicationSys.state + globalSiteReplicationSys.enabled = true + globalSiteReplicationSys.state = srState{ + Name: "cors-import-test", + ServiceAccountAccessKey: serviceCred.AccessKey, + Peers: map[string]madmin.PeerInfo{ + globalDeploymentID(): {Name: "local", DeploymentID: globalDeploymentID()}, + "peer-healthy": {Name: "healthy", DeploymentID: "peer-healthy", Endpoint: healthy.URL}, + "peer-broken": {Name: "broken", DeploymentID: "peer-broken", Endpoint: broken.URL}, + }, + } + globalSiteReplicationSys.Unlock() + defer func() { + globalSiteReplicationSys.Lock() + globalSiteReplicationSys.enabled, globalSiteReplicationSys.state = oldEnabled, oldState + globalSiteReplicationSys.Unlock() + }() + + rec := corsAdminRequest(t, cred, http.MethodPut, "/import-bucket-metadata", + corsZip(t, map[string][]byte{ + bucket + "/" + bucketCorsConfig: corsXML, + bucket + "/quota.json": []byte(`{"quota":8192,"quotatype":"hard"}`), + })) + st := corsImportReport(t, rec).Buckets[bucket] + if !st.Cors.IsSet || st.Cors.Err != "" { + t.Fatalf("%s: import report cors = %+v", instanceType, st.Cors) + } + stored, storedAt, err := globalBucketMetadataSys.GetCorsConfigXML(bucket) + if err != nil || !bytes.Equal(stored, corsXML) { + t.Fatalf("%s: stored CORS = %q, err = %v", instanceType, stored, err) + } + + // The reachable peer must have been told about the CORS document, + // carrying exactly the timestamp that was saved locally. + var corsSeen, sharedSeen bool + for range 2 { + select { + case item := <-healthyApplies: + if item.Type != madmin.SRBucketMetaTypeCorsConfig { + sharedSeen = item.Bucket == bucket && item.Quota != nil + continue + } + if item.Bucket != bucket || item.Cors == nil || !item.UpdatedAt.Equal(storedAt) { + t.Fatalf("%s: peer CORS event = %#v, want %s at %v", instanceType, item, bucket, storedAt) + } + payload, decErr := base64.StdEncoding.Strict().DecodeString(*item.Cors) + if decErr != nil || !bytes.Equal(payload, corsXML) { + t.Fatalf("%s: peer CORS payload = %q, err = %v", instanceType, payload, decErr) + } + corsSeen = true + case <-time.After(10 * time.Second): + t.Fatalf("%s: healthy peer received no further events (shared=%v cors=%v)", instanceType, sharedSeen, corsSeen) + } + } + if !sharedSeen || !corsSeen { + t.Fatalf("%s: healthy peer events shared=%v cors=%v, want both", instanceType, sharedSeen, corsSeen) + } + + // Both hook failures against the unreachable peer stay reported. + if got := strings.Count(st.Err, "->broken:"); got != 2 { + t.Fatalf("%s: bucket error mentions the broken peer %d times, want 2: %q", instanceType, got, st.Err) + } + }}) +} diff --git a/cmd/admin-bucket-handlers.go b/cmd/admin-bucket-handlers.go index 4ea93878f..01984e4de 100644 --- a/cmd/admin-bucket-handlers.go +++ b/cmd/admin-bucket-handlers.go @@ -41,7 +41,7 @@ import ( xhttp "github.com/minio/minio/internal/http" "github.com/minio/minio/internal/kms" "github.com/minio/mux" - "github.com/minio/pkg/v3/policy" + "github.com/pgsty/silo-pkg/v3/policy" ) const ( @@ -417,6 +417,7 @@ func (a adminAPIHandlers) ExportBucketMetadataHandler(w http.ResponseWriter, r * bucketLifecycleConfig, bucketSSEConfig, bucketTaggingConfig, + bucketCorsConfig, bucketQuotaConfigFile, objectLockConfig, bucketVersioningConfig, @@ -517,6 +518,19 @@ func (a adminAPIHandlers) ExportBucketMetadataHandler(w http.ResponseWriter, r * return } rawDataFn(bytes.NewReader(configData), cfgPath, len(configData)) + case bucketCorsConfig: + // Export the stored document verbatim: GetBucketCors returns + // the bytes exactly as they were PUT, so the archive must + // round-trip them unchanged. + configData, _, err := globalBucketMetadataSys.GetCorsConfigXML(bucket) + if err != nil { + if errors.Is(err, errConfigNotFound) { + continue + } + writeErrorResponse(ctx, w, exportError(ctx, err, cfgFile, bucket), r.URL) + return + } + rawDataFn(bytes.NewReader(configData), cfgPath, len(configData)) case objectLockConfig: config, _, err := globalBucketMetadataSys.GetObjectLockConfig(bucket) if err != nil { @@ -589,6 +603,50 @@ type importMetaReport struct { madmin.BucketMetaImportErrs } +type importMetadataFields map[string]struct{} + +func (f importMetadataFields) add(configFile string) { + f[configFile] = struct{}{} +} + +func applyImportedBucketMetadata(dst *BucketMetadata, src BucketMetadata, fields importMetadataFields) { + for configFile := range fields { + switch configFile { + case bucketPolicyConfig: + dst.PolicyConfigJSON = bytes.Clone(src.PolicyConfigJSON) + dst.PolicyConfigUpdatedAt = src.PolicyConfigUpdatedAt + case bucketNotificationConfig: + dst.NotificationConfigXML = bytes.Clone(src.NotificationConfigXML) + dst.NotificationConfigUpdatedAt = src.NotificationConfigUpdatedAt + case bucketLifecycleConfig: + dst.LifecycleConfigXML = bytes.Clone(src.LifecycleConfigXML) + dst.LifecycleConfigUpdatedAt = src.LifecycleConfigUpdatedAt + case bucketSSEConfig: + dst.EncryptionConfigXML = bytes.Clone(src.EncryptionConfigXML) + dst.EncryptionConfigUpdatedAt = src.EncryptionConfigUpdatedAt + case bucketTaggingConfig: + dst.TaggingConfigXML = bytes.Clone(src.TaggingConfigXML) + dst.TaggingConfigUpdatedAt = src.TaggingConfigUpdatedAt + case bucketQuotaConfigFile: + dst.QuotaConfigJSON = bytes.Clone(src.QuotaConfigJSON) + dst.QuotaConfigUpdatedAt = src.QuotaConfigUpdatedAt + case bucketCorsConfig: + // The import stamps its fields before creating any missing bucket, + // and a CORS event stamped before bucket creation is discarded as + // belonging to an older incarnation, so the imported document takes + // the same monotonic timestamp a local PutBucketCors would assign. + dst.CorsConfigUpdatedAt = localCORSUpdatedAt(*dst, src.CorsConfigUpdatedAt) + dst.CorsConfigXML = bytes.Clone(src.CorsConfigXML) + case objectLockConfig: + dst.ObjectLockConfigXML = bytes.Clone(src.ObjectLockConfigXML) + dst.ObjectLockConfigUpdatedAt = src.ObjectLockConfigUpdatedAt + case bucketVersioningConfig: + dst.VersioningConfigXML = bytes.Clone(src.VersioningConfigXML) + dst.VersioningConfigUpdatedAt = src.VersioningConfigUpdatedAt + } + } +} + func (i *importMetaReport) SetStatus(bucket, fname string, err error) { st := i.Buckets[bucket] var errMsg string @@ -608,6 +666,8 @@ func (i *importMetaReport) SetStatus(bucket, fname string, err error) { st.Tagging = madmin.MetaStatus{IsSet: true, Err: errMsg} case bucketQuotaConfigFile: st.Quota = madmin.MetaStatus{IsSet: true, Err: errMsg} + case bucketCorsConfig: + st.Cors = madmin.MetaStatus{IsSet: true, Err: errMsg} case objectLockConfig: st.ObjectLock = madmin.MetaStatus{IsSet: true, Err: errMsg} case bucketVersioningConfig: @@ -649,6 +709,16 @@ func (a adminAPIHandlers) ImportBucketMetadataHandler(w http.ResponseWriter, r * } bucketMap := make(map[string]*BucketMetadata, len(zr.File)) + importedFields := make(map[string]importMetadataFields, len(zr.File)) + blockedBuckets := make(map[string]struct{}) + markImported := func(bucket, configFile string) { + fields := importedFields[bucket] + if fields == nil { + fields = make(importMetadataFields) + importedFields[bucket] = fields + } + fields.add(configFile) + } updatedAt := UTCNow() @@ -664,6 +734,7 @@ func (a adminAPIHandlers) ImportBucketMetadataHandler(w http.ResponseWriter, r * bucketMap[bucket] = &meta } else if err != errConfigNotFound { rpt.SetStatus(bucket, "", err) + blockedBuckets[bucket] = struct{}{} } } @@ -675,6 +746,9 @@ func (a adminAPIHandlers) ImportBucketMetadataHandler(w http.ResponseWriter, r * continue } bucket, fileName := slc[0], slc[1] + if _, blocked := blockedBuckets[bucket]; blocked { + continue + } if fileName == objectLockConfig { reader, err := file.Open() if err != nil { @@ -708,6 +782,7 @@ func (a adminAPIHandlers) ImportBucketMetadataHandler(w http.ResponseWriter, r * bucketMap[bucket].ObjectLockConfigXML = configData bucketMap[bucket].ObjectLockConfigUpdatedAt = updatedAt + markImported(bucket, fileName) rpt.SetStatus(bucket, fileName, nil) } } @@ -720,6 +795,9 @@ func (a adminAPIHandlers) ImportBucketMetadataHandler(w http.ResponseWriter, r * continue } bucket, fileName := slc[0], slc[1] + if _, blocked := blockedBuckets[bucket]; blocked { + continue + } if fileName == bucketVersioningConfig { reader, err := file.Open() if err != nil { @@ -764,6 +842,7 @@ func (a adminAPIHandlers) ImportBucketMetadataHandler(w http.ResponseWriter, r * bucketMap[bucket].VersioningConfigXML = configData bucketMap[bucket].VersioningConfigUpdatedAt = updatedAt + markImported(bucket, fileName) rpt.SetStatus(bucket, fileName, nil) } } @@ -781,6 +860,9 @@ func (a adminAPIHandlers) ImportBucketMetadataHandler(w http.ResponseWriter, r * continue } bucket, fileName := slc[0], slc[1] + if _, blocked := blockedBuckets[bucket]; blocked { + continue + } // create bucket if it does not exist yet. if _, ok := bucketMap[bucket]; !ok { @@ -813,6 +895,7 @@ func (a adminAPIHandlers) ImportBucketMetadataHandler(w http.ResponseWriter, r * bucketMap[bucket].NotificationConfigXML = configData bucketMap[bucket].NotificationConfigUpdatedAt = updatedAt + markImported(bucket, fileName) rpt.SetStatus(bucket, fileName, nil) case bucketPolicyConfig: // Error out if Content-Length is beyond allowed size. @@ -847,6 +930,7 @@ func (a adminAPIHandlers) ImportBucketMetadataHandler(w http.ResponseWriter, r * bucketMap[bucket].PolicyConfigJSON = configData bucketMap[bucket].PolicyConfigUpdatedAt = updatedAt + markImported(bucket, fileName) rpt.SetStatus(bucket, fileName, nil) case bucketLifecycleConfig: bucketLifecycle, err := lifecycle.ParseLifecycleConfig(io.LimitReader(reader, sz)) @@ -879,6 +963,7 @@ func (a adminAPIHandlers) ImportBucketMetadataHandler(w http.ResponseWriter, r * bucketMap[bucket].LifecycleConfigXML = configData bucketMap[bucket].LifecycleConfigUpdatedAt = updatedAt + markImported(bucket, fileName) rpt.SetStatus(bucket, fileName, nil) case bucketSSEConfig: // Parse bucket encryption xml @@ -917,6 +1002,7 @@ func (a adminAPIHandlers) ImportBucketMetadataHandler(w http.ResponseWriter, r * bucketMap[bucket].EncryptionConfigXML = configData bucketMap[bucket].EncryptionConfigUpdatedAt = updatedAt + markImported(bucket, fileName) rpt.SetStatus(bucket, fileName, nil) case bucketTaggingConfig: tags, err := tags.ParseBucketXML(io.LimitReader(reader, sz)) @@ -933,6 +1019,7 @@ func (a adminAPIHandlers) ImportBucketMetadataHandler(w http.ResponseWriter, r * bucketMap[bucket].TaggingConfigXML = configData bucketMap[bucket].TaggingConfigUpdatedAt = updatedAt + markImported(bucket, fileName) rpt.SetStatus(bucket, fileName, nil) case bucketQuotaConfigFile: data, err := io.ReadAll(reader) @@ -949,6 +1036,33 @@ func (a adminAPIHandlers) ImportBucketMetadataHandler(w http.ResponseWriter, r * bucketMap[bucket].QuotaConfigJSON = data bucketMap[bucket].QuotaConfigUpdatedAt = updatedAt + markImported(bucket, fileName) + rpt.SetStatus(bucket, fileName, nil) + case bucketCorsConfig: + if sz > maxBucketCorsSize { + rpt.SetStatus(bucket, fileName, errors.New(ErrEntityTooLarge.String())) + continue + } + + // Read one byte past the declared size: stopping exactly at sz + // leaves archive/zip short of EOF, so it never verifies the entry + // checksum and a corrupt entry carrying well formed XML would be + // stored as a valid document. The extra byte also lets the reader + // reject an entry longer than it declares. + corsData, err := io.ReadAll(io.LimitReader(reader, sz+1)) + if err != nil { + rpt.SetStatus(bucket, fileName, err) + continue + } + + if err = validateCORSReplicationPayload(corsData); err != nil { + rpt.SetStatus(bucket, fileName, fmt.Errorf("%s (%s)", errorCodes[ErrMalformedXML].Description, err)) + continue + } + + bucketMap[bucket].CorsConfigXML = corsData + bucketMap[bucket].CorsConfigUpdatedAt = updatedAt + markImported(bucket, fileName) rpt.SetStatus(bucket, fileName, nil) } } @@ -962,22 +1076,70 @@ func (a adminAPIHandlers) ImportBucketMetadataHandler(w http.ResponseWriter, r * } for bucket, meta := range bucketMap { - err := globalBucketMetadataSys.save(ctx, *meta) + fields := importedFields[bucket] + if len(fields) == 0 { + continue + } + var merged BucketMetadata + err := func() error { + lockCtx, unlock, err := lockBucketMetadata(ctx, objectAPI, bucket) + if err != nil { + return err + } + defer unlock() + merged, err = loadBucketMetadataParse(lockCtx, objectAPI, bucket, true) + if err != nil { + return err + } + applyImportedBucketMetadata(&merged, *meta, fields) + return globalBucketMetadataSys.saveMetadata(lockCtx, objectAPI, merged) + }() if err != nil { rpt.SetStatus(bucket, "", err) continue } - // Call site replication hook. - if err = globalSiteReplicationSys.BucketMetaHook(ctx, madmin.SRBucketMeta{ - Bucket: bucket, - Quota: meta.QuotaConfigJSON, - Policy: meta.PolicyConfigJSON, - Versioning: enc(meta.VersioningConfigXML), - Tags: enc(meta.TaggingConfigXML), - ObjectLockConfig: enc(meta.ObjectLockConfigXML), - SSEConfig: enc(meta.EncryptionConfigXML), - UpdatedAt: updatedAt, - }); err != nil { + *meta = merged + globalNotificationSys.LoadBucketMetadata(bgContext(ctx), bucket) + hook := madmin.SRBucketMeta{Bucket: bucket, UpdatedAt: updatedAt} + var hookNeeded bool + if _, ok := fields[bucketQuotaConfigFile]; ok { + hook.Quota = meta.QuotaConfigJSON + hookNeeded = true + } + if _, ok := fields[bucketPolicyConfig]; ok { + hook.Policy = meta.PolicyConfigJSON + hookNeeded = true + } + if _, ok := fields[bucketVersioningConfig]; ok { + hook.Versioning = enc(meta.VersioningConfigXML) + hookNeeded = true + } + if _, ok := fields[bucketTaggingConfig]; ok { + hook.Tags = enc(meta.TaggingConfigXML) + hookNeeded = true + } + if _, ok := fields[objectLockConfig]; ok { + hook.ObjectLockConfig = enc(meta.ObjectLockConfigXML) + hookNeeded = true + } + if _, ok := fields[bucketSSEConfig]; ok { + hook.SSEConfig = enc(meta.EncryptionConfigXML) + hookNeeded = true + } + if hookNeeded { + err = globalSiteReplicationSys.BucketMetaHook(ctx, hook) + } + if _, ok := fields[bucketCorsConfig]; ok { + // CORS carries its own timestamp, so it replicates through the + // dedicated event rather than the shared bucket metadata hook. It + // is announced even when the shared hook failed: the document is + // already committed locally, and a peer that is unreachable for + // one config must not withhold CORS from the reachable ones. + if corsEvent, live := newBucketCORSReplicationEvent(bucket, *meta); live { + err = errors.Join(err, globalSiteReplicationSys.BucketMetaHook(ctx, corsEvent)) + } + } + if err != nil { rpt.SetStatus(bucket, "", err) continue } diff --git a/cmd/admin-handler-utils.go b/cmd/admin-handler-utils.go index cdfb79873..85ddae4ca 100644 --- a/cmd/admin-handler-utils.go +++ b/cmd/admin-handler-utils.go @@ -27,7 +27,7 @@ import ( "github.com/minio/madmin-go/v3" "github.com/minio/minio/internal/auth" "github.com/minio/minio/internal/config" - "github.com/minio/pkg/v3/policy" + "github.com/pgsty/silo-pkg/v3/policy" ) // validateAdminReq will validate request against and return whether it is allowed. diff --git a/cmd/admin-handlers-config-kv.go b/cmd/admin-handlers-config-kv.go index 2169103db..0a48f8c40 100644 --- a/cmd/admin-handlers-config-kv.go +++ b/cmd/admin-handlers-config-kv.go @@ -37,7 +37,7 @@ import ( "github.com/minio/minio/internal/config/subnet" "github.com/minio/minio/internal/logger" "github.com/minio/mux" - "github.com/minio/pkg/v3/policy" + "github.com/pgsty/silo-pkg/v3/policy" ) // DelConfigKVHandler - DELETE /minio/admin/v3/del-config-kv diff --git a/cmd/admin-handlers-idp-config.go b/cmd/admin-handlers-idp-config.go index 7b5792f03..8803cc546 100644 --- a/cmd/admin-handlers-idp-config.go +++ b/cmd/admin-handlers-idp-config.go @@ -32,8 +32,8 @@ import ( cfgldap "github.com/minio/minio/internal/config/identity/ldap" "github.com/minio/minio/internal/config/identity/openid" "github.com/minio/mux" - "github.com/minio/pkg/v3/ldap" - "github.com/minio/pkg/v3/policy" + "github.com/pgsty/silo-pkg/v3/ldap" + "github.com/pgsty/silo-pkg/v3/policy" ) func addOrUpdateIDPHandler(ctx context.Context, w http.ResponseWriter, r *http.Request, isUpdate bool) { diff --git a/cmd/admin-handlers-idp-ldap.go b/cmd/admin-handlers-idp-ldap.go index 99512c119..0695f45f6 100644 --- a/cmd/admin-handlers-idp-ldap.go +++ b/cmd/admin-handlers-idp-ldap.go @@ -28,8 +28,8 @@ import ( "github.com/minio/madmin-go/v3" "github.com/minio/minio/internal/auth" "github.com/minio/mux" - xldap "github.com/minio/pkg/v3/ldap" - "github.com/minio/pkg/v3/policy" + xldap "github.com/pgsty/silo-pkg/v3/ldap" + "github.com/pgsty/silo-pkg/v3/policy" ) // ListLDAPPolicyMappingEntities lists users/groups mapped to given/all policies. diff --git a/cmd/admin-handlers-idp-openid.go b/cmd/admin-handlers-idp-openid.go index 7e2387832..094dfdc1c 100644 --- a/cmd/admin-handlers-idp-openid.go +++ b/cmd/admin-handlers-idp-openid.go @@ -25,7 +25,7 @@ import ( "github.com/minio/madmin-go/v3" "github.com/minio/minio-go/v7/pkg/set" - "github.com/minio/pkg/v3/policy" + "github.com/pgsty/silo-pkg/v3/policy" ) const dummyRoleARN = "dummy-internal" diff --git a/cmd/admin-handlers-pools.go b/cmd/admin-handlers-pools.go index c4f98c454..cb53806b7 100644 --- a/cmd/admin-handlers-pools.go +++ b/cmd/admin-handlers-pools.go @@ -27,8 +27,8 @@ import ( "strings" "github.com/minio/mux" - "github.com/minio/pkg/v3/env" - "github.com/minio/pkg/v3/policy" + "github.com/pgsty/silo-pkg/v3/env" + "github.com/pgsty/silo-pkg/v3/policy" ) var ( diff --git a/cmd/admin-handlers-site-replication.go b/cmd/admin-handlers-site-replication.go index bda093955..3be2dee76 100644 --- a/cmd/admin-handlers-site-replication.go +++ b/cmd/admin-handlers-site-replication.go @@ -33,7 +33,7 @@ import ( "github.com/minio/madmin-go/v3" xioutil "github.com/minio/minio/internal/ioutil" "github.com/minio/mux" - "github.com/minio/pkg/v3/policy" + "github.com/pgsty/silo-pkg/v3/policy" ) // SiteReplicationAdd - PUT /minio/admin/v3/site-replication/add @@ -255,9 +255,11 @@ func (a adminAPIHandlers) SRPeerReplicateBucketItem(w http.ResponseWriter, r *ht case madmin.SRBucketMetaTypeTags: err = globalSiteReplicationSys.PeerBucketTaggingHandler(ctx, item.Bucket, item.Tags, item.UpdatedAt) case madmin.SRBucketMetaTypeObjectLockConfig: - err = globalSiteReplicationSys.PeerBucketObjectLockConfigHandler(ctx, item.Bucket, item.ObjectLockConfig, item.UpdatedAt) + err = globalSiteReplicationSys.peerBucketObjectLockConfigItem(ctx, item) case madmin.SRBucketMetaTypeSSEConfig: err = globalSiteReplicationSys.PeerBucketSSEConfigHandler(ctx, item.Bucket, item.SSEConfig, item.UpdatedAt) + case madmin.SRBucketMetaTypeCorsConfig: + err = globalSiteReplicationSys.PeerBucketCorsConfigHandler(ctx, item.Bucket, item.Cors, item.UpdatedAt) case madmin.SRBucketMetaLCConfig: err = globalSiteReplicationSys.PeerBucketLCConfigHandler(ctx, item.Bucket, item.ExpiryLCConfig, item.UpdatedAt) } diff --git a/cmd/admin-handlers-users-race_test.go b/cmd/admin-handlers-users-race_test.go index 79474a01f..ccff23265 100644 --- a/cmd/admin-handlers-users-race_test.go +++ b/cmd/admin-handlers-users-race_test.go @@ -32,7 +32,7 @@ import ( "github.com/minio/madmin-go/v3" minio "github.com/minio/minio-go/v7" - "github.com/minio/pkg/v3/sync/errgroup" + "github.com/pgsty/silo-pkg/v3/sync/errgroup" ) func runAllIAMConcurrencyTests(suite *TestSuiteIAM, c *check) { diff --git a/cmd/admin-handlers-users.go b/cmd/admin-handlers-users.go index 8530046b9..779da5dc5 100644 --- a/cmd/admin-handlers-users.go +++ b/cmd/admin-handlers-users.go @@ -40,8 +40,8 @@ import ( "github.com/minio/minio/internal/config/dns" "github.com/minio/minio/internal/logger" "github.com/minio/mux" - xldap "github.com/minio/pkg/v3/ldap" - "github.com/minio/pkg/v3/policy" + xldap "github.com/pgsty/silo-pkg/v3/ldap" + "github.com/pgsty/silo-pkg/v3/policy" "github.com/puzpuzpuz/xsync/v3" ) @@ -355,18 +355,25 @@ func (a adminAPIHandlers) ListGroups(w http.ResponseWriter, r *http.Request) { } // SetGroupStatus - PUT /minio/admin/v3/set-group-status?group=mygroup1&status=enabled +func setGroupStatusAdminAction(status string) policy.AdminAction { + if madmin.GroupStatus(status) == madmin.GroupDisabled { + return policy.DisableGroupAdminAction + } + return policy.EnableGroupAdminAction +} + func (a adminAPIHandlers) SetGroupStatus(w http.ResponseWriter, r *http.Request) { ctx := r.Context() - objectAPI, _ := validateAdminReq(ctx, w, r, policy.EnableGroupAdminAction) - if objectAPI == nil { - return - } - vars := mux.Vars(r) group := vars["group"] status := vars["status"] + objectAPI, _ := validateAdminReq(ctx, w, r, setGroupStatusAdminAction(status)) + if objectAPI == nil { + return + } + var ( err error updatedAt time.Time @@ -398,18 +405,25 @@ func (a adminAPIHandlers) SetGroupStatus(w http.ResponseWriter, r *http.Request) } // SetUserStatus - PUT /minio/admin/v3/set-user-status?accessKey=&status=[enabled|disabled] +func setUserStatusAdminAction(status string) policy.AdminAction { + if madmin.AccountStatus(status) == madmin.AccountDisabled { + return policy.DisableUserAdminAction + } + return policy.EnableUserAdminAction +} + func (a adminAPIHandlers) SetUserStatus(w http.ResponseWriter, r *http.Request) { ctx := r.Context() - objectAPI, creds := validateAdminReq(ctx, w, r, policy.EnableUserAdminAction) - if objectAPI == nil { - return - } - vars := mux.Vars(r) accessKey := vars["accessKey"] status := vars["status"] + objectAPI, creds := validateAdminReq(ctx, w, r, setUserStatusAdminAction(status)) + if objectAPI == nil { + return + } + // you cannot enable or disable yourself. if accessKey == creds.AccessKey { writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, errInvalidArgument), r.URL) @@ -859,7 +873,7 @@ func (a adminAPIHandlers) UpdateServiceAccount(w http.ResponseWriter, r *http.Re var sp *policy.Policy if len(updateReq.NewPolicy) > 0 { - sp, err = policy.ParseConfig(bytes.NewReader(updateReq.NewPolicy)) + sp, err = policy.ParseConfigStrict(bytes.NewReader(updateReq.NewPolicy)) if err != nil { writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) return @@ -1729,7 +1743,7 @@ func (a adminAPIHandlers) AddCannedPolicy(w http.ResponseWriter, r *http.Request return } - iamPolicy, err := policy.ParseConfig(bytes.NewReader(iamPolicyBytes)) + iamPolicy, err := policy.ParseConfigStrict(bytes.NewReader(iamPolicyBytes)) if err != nil { writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) return @@ -2981,7 +2995,7 @@ func commonAddServiceAccount(r *http.Request, ldap bool) (context.Context, auth. var sp *policy.Policy if len(createReq.Policy) > 0 { - sp, err = policy.ParseConfig(bytes.NewReader(createReq.Policy)) + sp, err = policy.ParseConfigStrict(bytes.NewReader(createReq.Policy)) if err != nil { return ctx, auth.Credentials{}, newServiceAccountOpts{}, madmin.AddServiceAccountReq{}, "", toAdminAPIErr(ctx, err) } diff --git a/cmd/admin-handlers-users_test.go b/cmd/admin-handlers-users_test.go index 828264583..0759822d7 100644 --- a/cmd/admin-handlers-users_test.go +++ b/cmd/admin-handlers-users_test.go @@ -40,13 +40,54 @@ import ( "github.com/minio/minio-go/v7/pkg/set" "github.com/minio/minio-go/v7/pkg/signer" "github.com/minio/minio/internal/auth" - "github.com/minio/pkg/v3/env" + "github.com/pgsty/silo-pkg/v3/env" + "github.com/pgsty/silo-pkg/v3/policy" ) const ( testDefaultTimeout = 30 * time.Second ) +func TestSetUserStatusAdminAction(t *testing.T) { + tests := []struct { + name string + status string + want policy.AdminAction + }{ + {name: "enable", status: string(madmin.AccountEnabled), want: policy.EnableUserAdminAction}, + {name: "disable", status: string(madmin.AccountDisabled), want: policy.DisableUserAdminAction}, + {name: "invalid preserves authenticated default", status: "invalid", want: policy.EnableUserAdminAction}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := setUserStatusAdminAction(tt.status); got != tt.want { + t.Fatalf("setUserStatusAdminAction(%q) = %q, want %q", tt.status, got, tt.want) + } + }) + } +} + +func TestSetGroupStatusAdminAction(t *testing.T) { + tests := []struct { + name string + status string + want policy.AdminAction + }{ + {name: "enable", status: string(madmin.GroupEnabled), want: policy.EnableGroupAdminAction}, + {name: "disable", status: string(madmin.GroupDisabled), want: policy.DisableGroupAdminAction}, + {name: "invalid preserves authenticated default", status: "invalid", want: policy.EnableGroupAdminAction}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := setGroupStatusAdminAction(tt.status); got != tt.want { + t.Fatalf("setGroupStatusAdminAction(%q) = %q, want %q", tt.status, got, tt.want) + } + }) + } +} + // API suite container for IAM type TestSuiteIAM struct { TestSuiteCommon @@ -202,8 +243,11 @@ func TestIAMInternalIDPServerSuite(t *testing.T) { suite.SetUpSuite(c) suite.TestUserCreate(c) + suite.TestUserStatusActionAuthorization(c) + suite.TestGroupStatusActionAuthorization(c) suite.TestUserPolicyEscalationBug(c) suite.TestPolicyCreate(c) + suite.TestServiceAccountBareARNPolicyRejected(c) suite.TestCannedPolicies(c) suite.TestGroupAddRemove(c) suite.TestServiceAccountOpsByAdmin(c) @@ -312,6 +356,184 @@ func (s *TestSuiteIAM) TestUserCreate(c *check) { } } +func (s *TestSuiteIAM) TestUserStatusActionAuthorization(c *check) { + ctx, cancel := context.WithTimeout(context.Background(), testDefaultTimeout) + defer cancel() + + var createdUsers []string + var createdPolicies []string + defer func() { + for _, user := range createdUsers { + if err := s.adm.RemoveUser(ctx, user); err != nil { + c.Errorf("unable to remove test user %s: %v", user, err) + } + } + for _, policyName := range createdPolicies { + if err := s.adm.RemoveCannedPolicy(ctx, policyName); err != nil { + c.Errorf("unable to remove test policy %s: %v", policyName, err) + } + } + }() + + createUser := func() (string, string) { + accessKey, secretKey := mustGenerateCredentials(c) + if err := s.adm.SetUser(ctx, accessKey, secretKey, madmin.AccountEnabled); err != nil { + c.Fatalf("unable to create test user: %v", err) + } + createdUsers = append(createdUsers, accessKey) + return accessKey, secretKey + } + + createStatusClient := func(action policy.AdminAction) *madmin.AdminClient { + accessKey, secretKey := createUser() + policyName := getRandomBucketName() + policyBytes := fmt.Appendf(nil, `{ + "Version": "2012-10-17", + "Statement": [{ + "Effect": "Allow", + "Action": ["%s"] + }] +}`, action) + if err := s.adm.AddCannedPolicy(ctx, policyName, policyBytes); err != nil { + c.Fatalf("unable to add status policy: %v", err) + } + createdPolicies = append(createdPolicies, policyName) + if _, err := s.adm.AttachPolicy(ctx, madmin.PolicyAssociationReq{ + Policies: []string{policyName}, + User: accessKey, + }); err != nil { + c.Fatalf("unable to attach status policy: %v", err) + } + + client, err := madmin.NewWithOptions(s.endpoint, &madmin.Options{ + Creds: credentials.NewStaticV4(accessKey, secretKey, ""), + Secure: s.secure, + }) + if err != nil { + c.Fatalf("unable to create status admin client: %v", err) + } + client.SetCustomTransport(s.TestSuiteCommon.client.Transport) + return client + } + + targetAccessKey, _ := createUser() + disableClient := createStatusClient(policy.DisableUserAdminAction) + if err := disableClient.SetUserStatus(ctx, targetAccessKey, madmin.AccountDisabled); err != nil { + c.Fatalf("DisableUser-only client could not disable a user: %v", err) + } + if err := disableClient.SetUserStatus(ctx, targetAccessKey, madmin.AccountEnabled); err == nil || madmin.ToErrorResponse(err).Code != "AccessDenied" { + c.Fatalf("DisableUser-only client unexpectedly enabled a user: %v", err) + } + + enableClient := createStatusClient(policy.EnableUserAdminAction) + if err := enableClient.SetUserStatus(ctx, targetAccessKey, madmin.AccountEnabled); err != nil { + c.Fatalf("EnableUser-only client could not enable a user: %v", err) + } + if err := enableClient.SetUserStatus(ctx, targetAccessKey, madmin.AccountDisabled); err == nil || madmin.ToErrorResponse(err).Code != "AccessDenied" { + c.Fatalf("EnableUser-only client unexpectedly disabled a user: %v", err) + } +} + +func (s *TestSuiteIAM) TestGroupStatusActionAuthorization(c *check) { + ctx, cancel := context.WithTimeout(context.Background(), testDefaultTimeout) + defer cancel() + + var createdUsers []string + var createdPolicies []string + group := getRandomBucketName() + var groupCreated bool + defer func() { + if groupCreated { + if err := s.adm.UpdateGroupMembers(ctx, madmin.GroupAddRemove{ + Group: group, + Members: createdUsers[:1], + IsRemove: true, + }); err != nil { + c.Errorf("unable to remove group member: %v", err) + } + if err := s.adm.UpdateGroupMembers(ctx, madmin.GroupAddRemove{Group: group, IsRemove: true}); err != nil { + c.Errorf("unable to remove test group: %v", err) + } + } + for _, user := range createdUsers { + if err := s.adm.RemoveUser(ctx, user); err != nil { + c.Errorf("unable to remove test user %s: %v", user, err) + } + } + for _, policyName := range createdPolicies { + if err := s.adm.RemoveCannedPolicy(ctx, policyName); err != nil { + c.Errorf("unable to remove test policy %s: %v", policyName, err) + } + } + }() + + createUser := func() (string, string) { + accessKey, secretKey := mustGenerateCredentials(c) + if err := s.adm.SetUser(ctx, accessKey, secretKey, madmin.AccountEnabled); err != nil { + c.Fatalf("unable to create test user: %v", err) + } + createdUsers = append(createdUsers, accessKey) + return accessKey, secretKey + } + + targetAccessKey, _ := createUser() + if err := s.adm.UpdateGroupMembers(ctx, madmin.GroupAddRemove{ + Group: group, + Members: []string{targetAccessKey}, + }); err != nil { + c.Fatalf("unable to create test group: %v", err) + } + groupCreated = true + + createStatusClient := func(action policy.AdminAction) *madmin.AdminClient { + accessKey, secretKey := createUser() + policyName := getRandomBucketName() + policyBytes := fmt.Appendf(nil, `{ + "Version": "2012-10-17", + "Statement": [{ + "Effect": "Allow", + "Action": ["%s"] + }] +}`, action) + if err := s.adm.AddCannedPolicy(ctx, policyName, policyBytes); err != nil { + c.Fatalf("unable to add group status policy: %v", err) + } + createdPolicies = append(createdPolicies, policyName) + if _, err := s.adm.AttachPolicy(ctx, madmin.PolicyAssociationReq{ + Policies: []string{policyName}, + User: accessKey, + }); err != nil { + c.Fatalf("unable to attach group status policy: %v", err) + } + + client, err := madmin.NewWithOptions(s.endpoint, &madmin.Options{ + Creds: credentials.NewStaticV4(accessKey, secretKey, ""), + Secure: s.secure, + }) + if err != nil { + c.Fatalf("unable to create group status admin client: %v", err) + } + client.SetCustomTransport(s.TestSuiteCommon.client.Transport) + return client + } + + disableClient := createStatusClient(policy.DisableGroupAdminAction) + if err := disableClient.SetGroupStatus(ctx, group, madmin.GroupDisabled); err != nil { + c.Fatalf("DisableGroup-only client could not disable a group: %v", err) + } + if err := disableClient.SetGroupStatus(ctx, group, madmin.GroupEnabled); err == nil || madmin.ToErrorResponse(err).Code != "AccessDenied" { + c.Fatalf("DisableGroup-only client unexpectedly enabled a group: %v", err) + } + + enableClient := createStatusClient(policy.EnableGroupAdminAction) + if err := enableClient.SetGroupStatus(ctx, group, madmin.GroupEnabled); err != nil { + c.Fatalf("EnableGroup-only client could not enable a group: %v", err) + } + if err := enableClient.SetGroupStatus(ctx, group, madmin.GroupDisabled); err == nil || madmin.ToErrorResponse(err).Code != "AccessDenied" { + c.Fatalf("EnableGroup-only client unexpectedly disabled a group: %v", err) + } +} + func (s *TestSuiteIAM) TestUserPolicyEscalationBug(c *check) { ctx, cancel := context.WithTimeout(context.Background(), testDefaultTimeout) defer cancel() @@ -600,6 +822,20 @@ func (s *TestSuiteIAM) TestPolicyCreate(c *check) { c.Fatalf("invalid policy creation success") } + for i, resource := range []string{"arn:aws:s3:::", "*arn:aws:s3:::"} { + barePolicyBytes := fmt.Appendf(nil, `{ + "Version": "2012-10-17", + "Statement": [{ + "Effect": "Deny", + "Action": ["s3:GetObject"], + "Resource": ["%s"] + }] +}`, resource) + if err = s.adm.AddCannedPolicy(ctx, fmt.Sprintf("%s-bare-%d", policy, i), barePolicyBytes); err == nil { + c.Fatalf("bare ARN policy creation succeeded for %q", resource) + } + } + // 3. Create a user, associate policy and verify access accessKey, secretKey := mustGenerateCredentials(c) err = s.adm.SetUser(ctx, accessKey, secretKey, madmin.AccountEnabled) @@ -653,6 +889,51 @@ func (s *TestSuiteIAM) TestPolicyCreate(c *check) { } } +func (s *TestSuiteIAM) TestServiceAccountBareARNPolicyRejected(c *check) { + ctx, cancel := context.WithTimeout(context.Background(), testDefaultTimeout) + defer cancel() + + barePolicy := []byte(`{ + "Version": "2012-10-17", + "Statement": [{ + "Effect": "Allow", + "Action": ["s3:GetObject"], + "NotResource": ["arn:aws:s3:::"] + }] +}`) + if _, err := s.adm.AddServiceAccount(ctx, madmin.AddServiceAccountReq{ + TargetUser: globalActiveCred.AccessKey, + Policy: barePolicy, + }); err == nil { + c.Fatal("service account creation accepted a bare ARN policy") + } + + validPolicy := []byte(`{ + "Version": "2012-10-17", + "Statement": [{ + "Effect": "Allow", + "Action": ["s3:GetObject"], + "Resource": ["arn:aws:s3:::*"] + }] +}`) + credentials, err := s.adm.AddServiceAccount(ctx, madmin.AddServiceAccountReq{ + TargetUser: globalActiveCred.AccessKey, + Policy: validPolicy, + }) + if err != nil { + c.Fatalf("service account creation rejected an explicit resource: %v", err) + } + defer func() { + _ = s.adm.DeleteServiceAccount(ctx, credentials.AccessKey) + }() + + if err = s.adm.UpdateServiceAccount(ctx, credentials.AccessKey, madmin.UpdateServiceAccountReq{ + NewPolicy: barePolicy, + }); err == nil { + c.Fatal("service account update accepted a bare ARN policy") + } +} + func (s *TestSuiteIAM) TestCannedPolicies(c *check) { ctx, cancel := context.WithTimeout(context.Background(), testDefaultTimeout) defer cancel() diff --git a/cmd/admin-handlers.go b/cmd/admin-handlers.go index ce5383f50..e4f06bc9f 100644 --- a/cmd/admin-handlers.go +++ b/cmd/admin-handlers.go @@ -60,8 +60,8 @@ import ( "github.com/minio/minio/internal/kms" "github.com/minio/minio/internal/logger" "github.com/minio/mux" - xnet "github.com/minio/pkg/v3/net" - "github.com/minio/pkg/v3/policy" + xnet "github.com/pgsty/silo-pkg/v3/net" + "github.com/pgsty/silo-pkg/v3/policy" "github.com/secure-io/sio-go" "github.com/zeebo/xxh3" ) diff --git a/cmd/admin-server-info.go b/cmd/admin-server-info.go index 4a98f9ba6..d810dfd16 100644 --- a/cmd/admin-server-info.go +++ b/cmd/admin-server-info.go @@ -30,7 +30,7 @@ import ( "github.com/minio/madmin-go/v3" "github.com/minio/minio/internal/config" "github.com/minio/minio/internal/kms" - xnet "github.com/minio/pkg/v3/net" + xnet "github.com/pgsty/silo-pkg/v3/net" ) // getLocalServerProperty - returns madmin.ServerProperties for only the diff --git a/cmd/api-errors.go b/cmd/api-errors.go index cc1e50d26..566fc09b7 100644 --- a/cmd/api-errors.go +++ b/cmd/api-errors.go @@ -48,7 +48,7 @@ import ( levent "github.com/minio/minio/internal/config/lambda/event" "github.com/minio/minio/internal/event" "github.com/minio/minio/internal/hash" - "github.com/minio/pkg/v3/policy" + "github.com/pgsty/silo-pkg/v3/policy" ) // APIError structure @@ -1523,10 +1523,14 @@ var errorCodes = errorCodeMap{ Description: "Your Host header is malformed.", HTTPStatusCode: http.StatusBadRequest, }, + // The stored object cannot be served: a server-side data condition, not a + // successful partial read. Upstream maps it to http.StatusPartialContent + // (since ca6b4773e, 2017), which lets SDKs accept the XML error document + // as object content; SILO deliberately diverges and returns 500. ErrObjectTampered: { Code: "XMinioObjectTampered", Description: errObjectTampered.Error(), - HTTPStatusCode: http.StatusPartialContent, + HTTPStatusCode: http.StatusInternalServerError, }, ErrSiteReplicationInvalidRequest: { @@ -2169,6 +2173,10 @@ func toAPIErrorCode(ctx context.Context, err error) (apiErr APIErrorCode) { err = unwrapAll(err) switch err { + case errCompleteMultipartChecksumMismatch, errCompleteMultipartChecksumTypeMismatch: + apiErr = ErrBadDigest + case errMissingPartChecksum: + apiErr = ErrInvalidRequest case errInvalidArgument: apiErr = ErrAdminInvalidArgument case errNoSuchPolicy: @@ -2465,6 +2473,14 @@ func toAPIError(ctx context.Context, err error) APIError { } apiErr := errorCodes.ToAPIErr(toAPIErrorCode(ctx, err)) + switch { + case errors.Is(err, errCompleteMultipartChecksumMismatch): + apiErr.Description = strings.TrimPrefix(err.Error(), errCompleteMultipartChecksumMismatch.Error()+": ") + case errors.Is(err, errCompleteMultipartChecksumTypeMismatch): + apiErr.Description = strings.TrimPrefix(err.Error(), errCompleteMultipartChecksumTypeMismatch.Error()+": ") + case errors.Is(err, errMissingPartChecksum): + apiErr.Description = strings.TrimPrefix(err.Error(), errMissingPartChecksum.Error()+": ") + } switch apiErr.Code { case "NotImplemented": apiErr = APIError{ diff --git a/cmd/api-errors_test.go b/cmd/api-errors_test.go index fda913b64..dfe6a4640 100644 --- a/cmd/api-errors_test.go +++ b/cmd/api-errors_test.go @@ -39,6 +39,10 @@ var toAPIErrorTests = []struct { {err: ObjectNameInvalid{}, errCode: ErrInvalidObjectName}, {err: InvalidUploadID{}, errCode: ErrNoSuchUpload}, {err: InvalidPart{}, errCode: ErrInvalidPart}, + {err: errCompleteMultipartChecksumMismatch, errCode: ErrBadDigest}, + {err: errCompleteMultipartChecksumTypeMismatch, errCode: ErrBadDigest}, + {err: errMissingPartChecksum, errCode: ErrInvalidRequest}, + {err: hash.ChecksumMismatch{}, errCode: ErrContentChecksumMismatch}, {err: InsufficientReadQuorum{}, errCode: ErrSlowDownRead}, {err: InsufficientWriteQuorum{}, errCode: ErrSlowDownWrite}, {err: InvalidUploadIDKeyCombination{}, errCode: ErrNotImplemented}, diff --git a/cmd/api-headers.go b/cmd/api-headers.go index c2ca23fbf..b1dadd126 100644 --- a/cmd/api-headers.go +++ b/cmd/api-headers.go @@ -212,7 +212,10 @@ func setObjectHeaders(ctx context.Context, w http.ResponseWriter, objInfo Object } if rs == nil && opts.PartNumber > 0 { - rs = partNumberToRangeSpec(objInfo, opts.PartNumber) + rs, err = partNumberToRangeSpec(objInfo, opts.PartNumber) + if err != nil { + return err + } } // For providing ranged content diff --git a/cmd/api-response.go b/cmd/api-response.go index cf25fd980..0750e4f67 100644 --- a/cmd/api-response.go +++ b/cmd/api-response.go @@ -27,7 +27,6 @@ import ( "path" "strconv" "strings" - "time" "github.com/minio/minio/internal/amztime" "github.com/minio/minio/internal/crypto" @@ -35,8 +34,8 @@ import ( "github.com/minio/minio/internal/hash" xhttp "github.com/minio/minio/internal/http" "github.com/minio/minio/internal/logger" - "github.com/minio/pkg/v3/policy" xxml "github.com/minio/xxml" + "github.com/pgsty/silo-pkg/v3/policy" ) const ( @@ -380,6 +379,13 @@ type CopyObjectResponse struct { XMLName xml.Name `xml:"http://s3.amazonaws.com/doc/2006-03-01/ CopyObjectResult" json:"-"` LastModified string // time string of format "2006-01-02T15:04:05.000Z" ETag string // md5sum of the copied object. + + ChecksumCRC32 string `xml:",omitempty"` + ChecksumCRC32C string `xml:",omitempty"` + ChecksumSHA1 string `xml:",omitempty"` + ChecksumSHA256 string `xml:",omitempty"` + ChecksumCRC64NVME string `xml:",omitempty"` + ChecksumType string `xml:",omitempty"` } // CopyObjectPartResponse container returns ETag and LastModified of the successfully copied object @@ -387,6 +393,12 @@ type CopyObjectPartResponse struct { XMLName xml.Name `xml:"http://s3.amazonaws.com/doc/2006-03-01/ CopyPartResult" json:"-"` LastModified string // time string of format "2006-01-02T15:04:05.000Z" ETag string // md5sum of the copied object part. + + ChecksumCRC32 string `xml:",omitempty"` + ChecksumCRC32C string `xml:",omitempty"` + ChecksumSHA1 string `xml:",omitempty"` + ChecksumSHA256 string `xml:",omitempty"` + ChecksumCRC64NVME string `xml:",omitempty"` } // Initiator inherit from Owner struct, fields are same @@ -416,6 +428,7 @@ type CompleteMultipartUploadResponse struct { Key string ETag string + ChecksumType string `xml:"ChecksumType,omitempty"` ChecksumCRC32 string `xml:"ChecksumCRC32,omitempty"` ChecksumCRC32C string `xml:"ChecksumCRC32C,omitempty"` ChecksumSHA1 string `xml:"ChecksumSHA1,omitempty"` @@ -763,19 +776,30 @@ func generateListObjectsV2Response(ctx context.Context, bucket, prefix, token, n type metaCheckFn = func(name string, action policy.Action) (s3Err APIErrorCode) -// generates CopyObjectResponse from etag and lastModified time. -func generateCopyObjectResponse(etag string, lastModified time.Time) CopyObjectResponse { +// generates CopyObjectResponse from the committed object information. +func generateCopyObjectResponse(oi ObjectInfo, cs map[string]string) CopyObjectResponse { return CopyObjectResponse{ - ETag: "\"" + etag + "\"", - LastModified: amztime.ISO8601Format(lastModified.UTC()), + ETag: "\"" + oi.ETag + "\"", + LastModified: amztime.ISO8601Format(oi.ModTime.UTC()), + ChecksumCRC32: cs[hash.ChecksumCRC32.String()], + ChecksumCRC32C: cs[hash.ChecksumCRC32C.String()], + ChecksumSHA1: cs[hash.ChecksumSHA1.String()], + ChecksumSHA256: cs[hash.ChecksumSHA256.String()], + ChecksumCRC64NVME: cs[hash.ChecksumCRC64NVME.String()], + ChecksumType: cs[xhttp.AmzChecksumType], } } -// generates CopyObjectPartResponse from etag and lastModified time. -func generateCopyObjectPartResponse(etag string, lastModified time.Time) CopyObjectPartResponse { +// generates CopyObjectPartResponse from the uploaded part information. +func generateCopyObjectPartResponse(partInfo PartInfo) CopyObjectPartResponse { return CopyObjectPartResponse{ - ETag: "\"" + etag + "\"", - LastModified: amztime.ISO8601Format(lastModified.UTC()), + ETag: "\"" + partInfo.ETag + "\"", + LastModified: amztime.ISO8601Format(partInfo.LastModified.UTC()), + ChecksumCRC32: partInfo.ChecksumCRC32, + ChecksumCRC32C: partInfo.ChecksumCRC32C, + ChecksumSHA1: partInfo.ChecksumSHA1, + ChecksumSHA256: partInfo.ChecksumSHA256, + ChecksumCRC64NVME: partInfo.ChecksumCRC64NVME, } } @@ -797,6 +821,7 @@ func generateCompleteMultipartUploadResponse(bucket, key, location string, oi Ob Key: key, // AWS S3 quotes the ETag in XML, make sure we are compatible here. ETag: "\"" + oi.ETag + "\"", + ChecksumType: cs[xhttp.AmzChecksumType], ChecksumSHA1: cs[hash.ChecksumSHA1.String()], ChecksumSHA256: cs[hash.ChecksumSHA256.String()], ChecksumCRC32: cs[hash.ChecksumCRC32.String()], diff --git a/cmd/api-router.go b/cmd/api-router.go index 188dd854f..dd5d4b9ad 100644 --- a/cmd/api-router.go +++ b/cmd/api-router.go @@ -18,16 +18,23 @@ package cmd import ( + "context" + "errors" "net" "net/http" + "strconv" + "strings" consoleapi "github.com/minio/console/api" + bktcors "github.com/minio/minio/internal/bucket/cors" xhttp "github.com/minio/minio/internal/http" "github.com/minio/mux" - "github.com/minio/pkg/v3/wildcard" + "github.com/pgsty/silo-pkg/v3/wildcard" "github.com/rs/cors" ) +type bucketCorsAppliedKey struct{} + func newHTTPServerFn() *xhttp.Server { globalObjLayerMutex.RLock() defer globalObjLayerMutex.RUnlock() @@ -111,11 +118,6 @@ var rejectedBucketAPIs = []rejectedAPI{ methods: []string{http.MethodGet, http.MethodPut, http.MethodDelete}, queries: []string{"inventory", ""}, }, - { - api: "cors", - methods: []string{http.MethodPut, http.MethodDelete}, - queries: []string{"cors", ""}, - }, { api: "metrics", methods: []string{http.MethodGet, http.MethodPut, http.MethodDelete}, @@ -459,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", "") @@ -648,6 +650,94 @@ func registerAPIRouter(router *mux.Router) { apiRouter.MethodNotAllowedHandler = collectAPIStats("methodnotallowed", httpTraceAll(methodNotAllowedHandler("S3"))) } +// applyBucketCors applies a bucket's CORS configuration to the request. +// For an OPTIONS preflight it writes the full CORS response and returns true +// (request is complete). For an actual request it adds the applicable +// Access-Control-* response headers and returns false so the request +// continues down the handler chain. If no rule matches a preflight it writes +// 403 and returns true. A matched actual request is marked in its context so +// inner legacy middleware does not rewrite an explicitly allowed null origin. +func applyBucketCors(w http.ResponseWriter, r *http.Request, cfg *bktcors.Config) (handled bool) { + origin := r.Header.Get("Origin") + if origin == "" { + return false // not a CORS request + } + h := w.Header() + h.Add("Vary", "Origin") + + isPreflight := r.Method == http.MethodOptions && + r.Header.Get("Access-Control-Request-Method") != "" + + if isPreflight { + method := r.Header.Get("Access-Control-Request-Method") + reqHeaders := splitAndTrim(r.Header.Get("Access-Control-Request-Headers")) + // A preflight response depends on all three request headers that + // determine the outcome, including when the request is rejected. + h.Add("Vary", "Access-Control-Request-Method") + h.Add("Vary", "Access-Control-Request-Headers") + rule, allowedOrigin, allowedHeaders, maxAgeSeconds, ok := cfg.MatchPreflight(origin, method, reqHeaders) + if !ok { + writeResponse(w, http.StatusForbidden, nil, mimeNone) + return true + } + setBucketCorsOriginHeaders(h, allowedOrigin, origin) + h.Set("Access-Control-Allow-Methods", strings.Join(rule.AllowedMethods, ", ")) + if len(allowedHeaders) > 0 { + h.Set("Access-Control-Allow-Headers", strings.Join(allowedHeaders, ", ")) + } + if len(rule.ExposeHeaders) > 0 { + h.Set("Access-Control-Expose-Headers", strings.Join(rule.ExposeHeaders, ", ")) + } + if maxAgeSeconds != nil { + h.Set("Access-Control-Max-Age", strconv.Itoa(*maxAgeSeconds)) + } + writeResponse(w, http.StatusOK, nil, mimeNone) + return true + } + + // Actual request: attach headers if the origin+method match. + rule, allowedOrigin, ok := cfg.MatchRule(origin, r.Method) + if !ok { + return false // no matching rule → no CORS headers, continue normally + } + *r = *r.WithContext(context.WithValue(r.Context(), bucketCorsAppliedKey{}, struct{}{})) + setBucketCorsOriginHeaders(h, allowedOrigin, origin) + if len(rule.ExposeHeaders) > 0 { + h.Set("Access-Control-Expose-Headers", strings.Join(rule.ExposeHeaders, ", ")) + } + return false +} + +func bucketCorsWasApplied(r *http.Request) bool { + _, ok := r.Context().Value(bucketCorsAppliedKey{}).(struct{}) + return ok +} + +func setBucketCorsOriginHeaders(h http.Header, allowedOrigin, requestOrigin string) { + if allowedOrigin == "*" { + h.Set("Access-Control-Allow-Origin", "*") + h.Del("Access-Control-Allow-Credentials") + return + } + h.Set("Access-Control-Allow-Origin", requestOrigin) + h.Set("Access-Control-Allow-Credentials", "true") +} + +// splitAndTrim splits a comma-separated header list into trimmed, non-empty values. +func splitAndTrim(s string) []string { + if s == "" { + return nil + } + parts := strings.Split(s, ",") + out := parts[:0] + for _, p := range parts { + if p = strings.TrimSpace(p); p != "" { + out = append(out, p) + } + } + return out +} + // corsHandler handler for CORS (Cross Origin Resource Sharing) func corsHandler(handler http.Handler) http.Handler { commonS3Headers := []string{ @@ -693,5 +783,32 @@ func corsHandler(handler http.Handler) http.Handler { ExposedHeaders: commonS3Headers, AllowCredentials: true, } - return cors.New(opts).Handler(handler) + globalCors := cors.New(opts).Handler(handler) + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Header.Get("Origin") != "" { + if bucket, _ := request2BucketObjectName(r); bucket != "" && globalBucketMetadataSys != nil { + // 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) { + return + } + handler.ServeHTTP(w, r) + return + } + if err != nil && !errors.Is(err, errConfigNotFound) { + internalLogOnceIf(r.Context(), err, "bucket-cors-metadata") + handler.ServeHTTP(w, r) + return + } + } + } + globalCors.ServeHTTP(w, r) + }) } diff --git a/cmd/auth-handler.go b/cmd/auth-handler.go index d5df28624..884fa9c83 100644 --- a/cmd/auth-handler.go +++ b/cmd/auth-handler.go @@ -41,7 +41,7 @@ import ( xjwt "github.com/minio/minio/internal/jwt" "github.com/minio/minio/internal/logger" "github.com/minio/minio/internal/mcontext" - "github.com/minio/pkg/v3/policy" + "github.com/pgsty/silo-pkg/v3/policy" ) // Verify if request has JWT. @@ -363,17 +363,6 @@ func checkRequestAuthTypeWithRequestTags(ctx context.Context, r *http.Request, a return authorizeRequestWithTags(ctx, r, action, "", requestTags) } -// checkRequestAuthTypeWithVID is similar to checkRequestAuthType -// passes versionID additionally. -func checkRequestAuthTypeWithVID(ctx context.Context, r *http.Request, action policy.Action, bucketName, objectName, versionID string) (s3Err APIErrorCode) { - logger.GetReqInfo(ctx).BucketName = bucketName - logger.GetReqInfo(ctx).ObjectName = objectName - logger.GetReqInfo(ctx).VersionID = versionID - - _, _, s3Err = checkRequestAuthTypeCredential(ctx, r, action) - return s3Err -} - func authenticateRequest(ctx context.Context, r *http.Request, action policy.Action) (s3Err APIErrorCode) { if logger.GetReqInfo(ctx) == nil { bugLogIf(ctx, errors.New("unexpected context.Context does not have a logger.ReqInfo"), logger.ErrorKind) @@ -439,6 +428,23 @@ func authorizeRequest(ctx context.Context, r *http.Request, action policy.Action return authorizeRequestWithExistingTags(ctx, r, action, "") } +func deleteObjectAction(versionID string) policy.Action { + if versionID != "" { + return policy.DeleteObjectVersionAction + } + return policy.DeleteObjectAction +} + +func actionUsesObjectVersion(action policy.Action) bool { + switch action { + case policy.DeleteObjectAction, policy.DeleteObjectVersionAction, + policy.ReplicateDeleteAction, policy.BypassGovernanceRetentionAction: + return true + default: + return false + } +} + func authorizeRequestWithExistingTags(ctx context.Context, r *http.Request, action policy.Action, existingTags string) (s3Err APIErrorCode) { return authorizeRequestWithTags(ctx, r, action, existingTags, nil) } @@ -457,7 +463,7 @@ func authorizeRequestWithTags(ctx context.Context, r *http.Request, action polic versionID := reqInfo.VersionID conditionValuesForAuth := func(locationConstraint string, credentials auth.Credentials) map[string][]string { values := getConditionValuesWithTags(r, locationConstraint, credentials, existingTags, requestTags) - if action == policy.DeleteObjectAction { + if actionUsesObjectVersion(action) { // DeleteObjects carries the effective version ID in each XML object, // not in the request query. Keep authorization scoped to that entry. if versionID == "" { @@ -503,21 +509,6 @@ func authorizeRequestWithTags(ctx context.Context, r *http.Request, action polic return ErrAccessDenied } - if action == policy.DeleteObjectAction && versionID != "" { - if !globalIAMSys.IsAllowed(policy.Args{ - AccountName: cred.AccessKey, - Groups: cred.Groups, - Action: policy.Action(policy.DeleteObjectVersionAction), - BucketName: bucket, - ConditionValues: conditionValuesForAuth("", cred), - ObjectName: object, - IsOwner: owner, - Claims: cred.Claims, - DenyOnly: true, - }) { // Request is not allowed if Deny action on DeleteObjectVersionAction - return ErrAccessDenied - } - } if globalIAMSys.IsAllowed(policy.Args{ AccountName: cred.AccessKey, Groups: cred.Groups, @@ -553,6 +544,40 @@ func authorizeRequestWithTags(ctx context.Context, r *http.Request, action polic return ErrAccessDenied } +// authorizeReplicationDelete preserves the established target-credential +// contract for trusted replication: DeleteObject and ReplicateDelete must be +// allowed, while an explicit DeleteObjectVersion deny still blocks a named +// version. Ordinary S3 requests never use this compatibility path. +func authorizeReplicationDelete(ctx context.Context, r *http.Request) APIErrorCode { + if s3Err := authorizeRequest(ctx, r, policy.DeleteObjectAction); s3Err != ErrNone { + return s3Err + } + reqInfo := logger.GetReqInfo(ctx) + if reqInfo == nil { + return ErrAccessDenied + } + if reqInfo.VersionID == "" { + return ErrNone + } + cred := reqInfo.Cred + values := getConditionValuesWithTags(r, "", cred, "", nil) + values["versionid"] = []string{reqInfo.VersionID} + if !globalIAMSys.IsAllowed(policy.Args{ + AccountName: cred.AccessKey, + Groups: cred.Groups, + Action: policy.DeleteObjectVersionAction, + BucketName: reqInfo.BucketName, + ConditionValues: values, + ObjectName: reqInfo.ObjectName, + IsOwner: reqInfo.Owner, + Claims: cred.Claims, + DenyOnly: true, + }) { + return ErrAccessDenied + } + return ErrNone +} + // Check request auth type verifies the incoming http request // - validates the request signature // - validates the policy action if anonymous tests bucket policies if any, @@ -786,10 +811,20 @@ func isPutActionAllowedWithRequestTags(ctx context.Context, atype authType, buck return s3Err } - logger.GetReqInfo(ctx).Cred = cred - logger.GetReqInfo(ctx).Owner = owner - logger.GetReqInfo(ctx).Region = region + reqInfo := logger.GetReqInfo(ctx) + if reqInfo == nil { + return ErrAccessDenied + } + reqInfo.Lock() + reqInfo.Cred = cred + reqInfo.Owner = owner + reqInfo.Region = region + reqInfo.Unlock() + return isPutActionAllowedWithCred(bucketName, objectName, r, action, requestTags, cred, owner) +} + +func isPutActionAllowedWithCred(bucketName, objectName string, r *http.Request, action policy.Action, requestTags *string, cred auth.Credentials, owner bool) APIErrorCode { // Do not check for PutObjectRetentionAction permission, // if mode and retain until date are not set. // Can happen when bucket has default lock config set diff --git a/cmd/auth-handler_test.go b/cmd/auth-handler_test.go index 47f12add6..454524aa8 100644 --- a/cmd/auth-handler_test.go +++ b/cmd/auth-handler_test.go @@ -28,7 +28,7 @@ import ( "time" "github.com/minio/minio/internal/auth" - "github.com/minio/pkg/v3/policy" + "github.com/pgsty/silo-pkg/v3/policy" ) type nullReader struct{} diff --git a/cmd/background-heal-ops.go b/cmd/background-heal-ops.go index 3eeff5098..279d86a47 100644 --- a/cmd/background-heal-ops.go +++ b/cmd/background-heal-ops.go @@ -25,7 +25,7 @@ import ( "time" "github.com/minio/madmin-go/v3" - "github.com/minio/pkg/v3/env" + "github.com/pgsty/silo-pkg/v3/env" ) // healTask represents what to heal along with options diff --git a/cmd/background-newdisks-heal-ops.go b/cmd/background-newdisks-heal-ops.go index 330bace41..454234ba6 100644 --- a/cmd/background-newdisks-heal-ops.go +++ b/cmd/background-newdisks-heal-ops.go @@ -34,7 +34,7 @@ import ( "github.com/minio/madmin-go/v3" "github.com/minio/minio-go/v7/pkg/set" "github.com/minio/minio/internal/config" - "github.com/minio/pkg/v3/env" + "github.com/pgsty/silo-pkg/v3/env" ) const ( diff --git a/cmd/batch-expire.go b/cmd/batch-expire.go index 99fed54e0..6de32920a 100644 --- a/cmd/batch-expire.go +++ b/cmd/batch-expire.go @@ -33,10 +33,10 @@ import ( "github.com/minio/minio/internal/bucket/versioning" xhttp "github.com/minio/minio/internal/http" xioutil "github.com/minio/minio/internal/ioutil" - "github.com/minio/pkg/v3/env" - "github.com/minio/pkg/v3/wildcard" - "github.com/minio/pkg/v3/workers" - "github.com/minio/pkg/v3/xtime" + "github.com/pgsty/silo-pkg/v3/env" + "github.com/pgsty/silo-pkg/v3/wildcard" + "github.com/pgsty/silo-pkg/v3/workers" + "github.com/pgsty/silo-pkg/v3/xtime" "go.yaml.in/yaml/v3" ) diff --git a/cmd/batch-handlers.go b/cmd/batch-handlers.go index 7614a2f24..69d2394f1 100644 --- a/cmd/batch-handlers.go +++ b/cmd/batch-handlers.go @@ -48,10 +48,10 @@ import ( "github.com/minio/minio/internal/hash" xhttp "github.com/minio/minio/internal/http" xioutil "github.com/minio/minio/internal/ioutil" - "github.com/minio/pkg/v3/console" - "github.com/minio/pkg/v3/env" - "github.com/minio/pkg/v3/policy" - "github.com/minio/pkg/v3/workers" + "github.com/pgsty/silo-pkg/v3/console" + "github.com/pgsty/silo-pkg/v3/env" + "github.com/pgsty/silo-pkg/v3/policy" + "github.com/pgsty/silo-pkg/v3/workers" "go.yaml.in/yaml/v3" ) diff --git a/cmd/batch-job-common-types.go b/cmd/batch-job-common-types.go index f76e7644b..c8a72d6ba 100644 --- a/cmd/batch-job-common-types.go +++ b/cmd/batch-job-common-types.go @@ -23,7 +23,7 @@ import ( "time" "github.com/dustin/go-humanize" - "github.com/minio/pkg/v3/wildcard" + "github.com/pgsty/silo-pkg/v3/wildcard" "go.yaml.in/yaml/v3" ) diff --git a/cmd/batch-replicate.go b/cmd/batch-replicate.go index 37a1834d4..c028f5215 100644 --- a/cmd/batch-replicate.go +++ b/cmd/batch-replicate.go @@ -22,7 +22,7 @@ import ( miniogo "github.com/minio/minio-go/v7" "github.com/minio/minio/internal/auth" - "github.com/minio/pkg/v3/xtime" + "github.com/pgsty/silo-pkg/v3/xtime" ) //go:generate msgp -file $GOFILE diff --git a/cmd/batch-rotate.go b/cmd/batch-rotate.go index 3e8f18faf..a238af08b 100644 --- a/cmd/batch-rotate.go +++ b/cmd/batch-rotate.go @@ -34,8 +34,8 @@ import ( "github.com/minio/minio/internal/crypto" xhttp "github.com/minio/minio/internal/http" "github.com/minio/minio/internal/kms" - "github.com/minio/pkg/v3/env" - "github.com/minio/pkg/v3/workers" + "github.com/pgsty/silo-pkg/v3/env" + "github.com/pgsty/silo-pkg/v3/workers" ) // keyrotate: diff --git a/cmd/bootstrap-peer-server.go b/cmd/bootstrap-peer-server.go index 13de6f70c..c6320aaa5 100644 --- a/cmd/bootstrap-peer-server.go +++ b/cmd/bootstrap-peer-server.go @@ -34,7 +34,7 @@ import ( "github.com/minio/minio-go/v7/pkg/set" "github.com/minio/minio/internal/grid" "github.com/minio/minio/internal/logger" - "github.com/minio/pkg/v3/env" + "github.com/pgsty/silo-pkg/v3/env" ) // To abstract a node over network. diff --git a/cmd/bucket-cors-adversarial_test.go b/cmd/bucket-cors-adversarial_test.go new file mode 100644 index 000000000..49c0fe9b1 --- /dev/null +++ b/cmd/bucket-cors-adversarial_test.go @@ -0,0 +1,235 @@ +// Copyright (c) 2015-2021 MinIO, Inc. +// +// This file is part of MinIO Object Storage stack +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +package cmd + +import ( + "bytes" + "encoding/base64" + "encoding/binary" + "hash/crc32" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/minio/minio/internal/auth" +) + +func TestPutBucketCorsWireValidation(t *testing.T) { + ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{ + t: t, + objAPITest: testPutBucketCorsWireValidation, + endpoints: []string{"PutBucketCors"}, + }) +} + +func testPutBucketCorsWireValidation(_ ObjectLayer, _ string, bucketName string, apiRouter http.Handler, + creds auth.Credentials, t *testing.T, +) { + valid := `*GET` + rule := `*GET` + tests := []struct { + name string + body string + want int + wantCode string + }{ + { + name: "second XML root", + body: valid + ``, + want: http.StatusBadRequest, + wantCode: "MalformedXML", + }, + { + name: "255 Unicode character ID", + body: `` + strings.Repeat("界", 255) + `*GET`, + want: http.StatusOK, + }, + { + name: "256 Unicode character ID", + body: `` + strings.Repeat("界", 256) + `*GET`, + want: http.StatusBadRequest, + wantCode: "MalformedXML", + }, + { + name: "lowercase method", + body: `*get`, + want: http.StatusBadRequest, + wantCode: "MalformedXML", + }, + { + name: "empty origin", + body: `GET`, + want: http.StatusBadRequest, + wantCode: "MalformedXML", + }, + { + name: "question mark origin wildcard", + body: `https://?.example.comGET`, + want: http.StatusBadRequest, + wantCode: "MalformedXML", + }, + { + name: "question mark header wildcard", + body: `*GETx-amz-?`, + want: http.StatusBadRequest, + wantCode: "MalformedXML", + }, + { + name: "unknown element", + body: `*GET`, + want: http.StatusBadRequest, + wantCode: "MalformedXML", + }, + { + name: "empty max age", + body: `*GET`, + want: http.StatusBadRequest, + wantCode: "MalformedXML", + }, + { + name: "zero max age", + body: `*GET0`, + want: http.StatusOK, + }, + { + name: "max age int32 overflow", + body: `*GET2147483648`, + want: http.StatusBadRequest, + wantCode: "MalformedXML", + }, + { + name: "100 rules", + body: `` + strings.Repeat(rule, 100) + ``, + want: http.StatusOK, + }, + { + name: "101 rules", + body: `` + strings.Repeat(rule, 101) + ``, + want: http.StatusBadRequest, + wantCode: "MalformedXML", + }, + { + name: "exactly 64 KiB", + body: sizedCORSConfig(maxBucketCorsSize), + want: http.StatusOK, + }, + { + name: "over 64 KiB", + body: sizedCORSConfig(maxBucketCorsSize + 1), + want: http.StatusBadRequest, + wantCode: "EntityTooLarge", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + req, err := newTestSignedRequestV4(http.MethodPut, getBucketCorsURL("", bucketName), + int64(len(tt.body)), bytes.NewReader([]byte(tt.body)), creds.AccessKey, creds.SecretKey, nil) + if err != nil { + t.Fatal(err) + } + rec := httptest.NewRecorder() + apiRouter.ServeHTTP(rec, req) + if rec.Code != tt.want { + t.Fatalf("expected status %d, got %d: %s", tt.want, rec.Code, rec.Body.String()) + } + if tt.wantCode != "" && !bytes.Contains(rec.Body.Bytes(), []byte(""+tt.wantCode+"")) { + t.Fatalf("expected error code %s, got: %s", tt.wantCode, rec.Body.String()) + } + }) + } +} + +func sizedCORSConfig(size int) string { + prefix := `*GET` + suffix := `` + return prefix + strings.Repeat(" ", size-len(prefix)-len(suffix)) + suffix +} + +func TestPutBucketCorsChecksumValidation(t *testing.T) { + ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{ + t: t, + objAPITest: testPutBucketCorsChecksumValidation, + endpoints: []string{"PutBucketCors"}, + }) +} + +func testPutBucketCorsChecksumValidation(_ ObjectLayer, _ string, bucketName string, apiRouter http.Handler, + creds auth.Credentials, t *testing.T, +) { + body := []byte(`*GET`) + tests := []struct { + name string + configure func(*http.Request) + want int + wantCode string + }{ + { + name: "missing checksum", + configure: func(req *http.Request) { + req.Header.Del("Content-Md5") + }, + want: http.StatusBadRequest, + wantCode: "MissingContentMD5", + }, + { + name: "bad content md5", + configure: func(req *http.Request) { + req.Header.Set("Content-Md5", getMD5HashBase64([]byte("different body"))) + }, + want: http.StatusBadRequest, + wantCode: "BadDigest", + }, + { + name: "valid sdk crc32", + configure: func(req *http.Request) { + req.Header.Del("Content-Md5") + req.Header.Set("X-Amz-Sdk-Checksum-Algorithm", "CRC32") + req.Header.Set("X-Amz-Checksum-Crc32", corsCRC32Base64(body)) + }, + want: http.StatusOK, + }, + { + name: "bad sdk crc32", + configure: func(req *http.Request) { + req.Header.Del("Content-Md5") + req.Header.Set("X-Amz-Sdk-Checksum-Algorithm", "CRC32") + req.Header.Set("X-Amz-Checksum-Crc32", corsCRC32Base64([]byte("different body"))) + }, + want: http.StatusBadRequest, + wantCode: "BadDigest", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + req, err := newTestRequest(http.MethodPut, getBucketCorsURL("", bucketName), int64(len(body)), bytes.NewReader(body)) + if err != nil { + t.Fatal(err) + } + tt.configure(req) + if err = signRequestV4(req, creds.AccessKey, creds.SecretKey); err != nil { + t.Fatal(err) + } + rec := httptest.NewRecorder() + apiRouter.ServeHTTP(rec, req) + if rec.Code != tt.want || (tt.wantCode != "" && !bytes.Contains(rec.Body.Bytes(), []byte(""+tt.wantCode+""))) { + t.Fatalf("expected status %d and code %s, got %d: %s", tt.want, tt.wantCode, rec.Code, rec.Body.String()) + } + }) + } +} + +func corsCRC32Base64(data []byte) string { + var checksum [4]byte + binary.BigEndian.PutUint32(checksum[:], crc32.ChecksumIEEE(data)) + return base64.StdEncoding.EncodeToString(checksum[:]) +} diff --git a/cmd/bucket-cors-handlers.go b/cmd/bucket-cors-handlers.go new file mode 100644 index 000000000..b22616bb6 --- /dev/null +++ b/cmd/bucket-cors-handlers.go @@ -0,0 +1,203 @@ +// Copyright (c) 2015-2021 MinIO, Inc. +// +// This file is part of MinIO Object Storage stack +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package cmd + +import ( + "bytes" + "encoding/base64" + "errors" + "io" + "net/http" + + humanize "github.com/dustin/go-humanize" + "github.com/minio/madmin-go/v3" + "github.com/minio/minio/internal/bucket/cors" + hashpkg "github.com/minio/minio/internal/hash" + "github.com/minio/minio/internal/logger" + "github.com/minio/mux" + "github.com/pgsty/silo-pkg/v3/policy" +) + +// maxBucketCorsSize is the maximum allowed size of a CORS configuration document. +const maxBucketCorsSize = 64 * humanize.KiByte + +// PutBucketCorsHandler - PUT bucket cors. +func (api objectAPIHandlers) PutBucketCorsHandler(w http.ResponseWriter, r *http.Request) { + ctx := newContext(r, w, "PutBucketCors") + + defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r)) + + objAPI := api.ObjectAPI() + if objAPI == nil { + writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL) + return + } + + vars := mux.Vars(r) + bucket := vars["bucket"] + + if s3Error := checkRequestAuthType(ctx, r, policy.PutBucketCorsAction, bucket, ""); s3Error != ErrNone { + writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL) + return + } + + if _, err := objAPI.GetBucketInfo(ctx, bucket, BucketOptions{}); err != nil { + writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL) + return + } + + if r.ContentLength <= 0 { + writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrMissingContentLength), r.URL) + return + } + if r.ContentLength > maxBucketCorsSize { + writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrEntityTooLarge), r.URL) + return + } + + // PutBucketCors requires a Content-Md5 or a supported full-header + // checksum. validateLengthAndChecksum wraps r.Body so the supplied digest + // is verified as the body is read below. + if !validateLengthAndChecksum(r) { + writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrMissingContentMD5), r.URL) + return + } + + corsBytes, err := io.ReadAll(r.Body) + if err != nil { + if errors.Is(err, hashpkg.ErrInvalidChecksum) { + writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrBadDigest), r.URL) + return + } + writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL) + return + } + + corsCfg, err := cors.ParseBucketCorsConfig(bytes.NewReader(corsBytes)) + if err != nil { + writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrMalformedXML), r.URL) + return + } + if err := corsCfg.Validate(); err != nil { + writeErrorResponse(ctx, w, APIError{ + Code: "MalformedXML", + HTTPStatusCode: http.StatusBadRequest, + Description: err.Error(), + }, r.URL) + return + } + + updatedAt, err := updateLocalBucketCORSMetadata(ctx, objAPI, bucket, corsBytes) + if err != nil { + writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL) + return + } + + // Call site replication hook. + // + // We encode the xml bytes as base64 to ensure there are no encoding + // errors. + cfgStr := base64.StdEncoding.EncodeToString(corsBytes) + replLogIf(ctx, globalSiteReplicationSys.BucketMetaHook(ctx, madmin.SRBucketMeta{ + Type: madmin.SRBucketMetaTypeCorsConfig, + Bucket: bucket, + Cors: &cfgStr, + UpdatedAt: updatedAt, + })) + + writeSuccessResponseHeadersOnly(w) +} + +// GetBucketCorsHandler - GET bucket cors. +func (api objectAPIHandlers) GetBucketCorsHandler(w http.ResponseWriter, r *http.Request) { + ctx := newContext(r, w, "GetBucketCors") + + defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r)) + + objAPI := api.ObjectAPI() + if objAPI == nil { + writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL) + return + } + + vars := mux.Vars(r) + bucket := vars["bucket"] + + if s3Error := checkRequestAuthType(ctx, r, policy.GetBucketCorsAction, bucket, ""); s3Error != ErrNone { + writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL) + return + } + + if _, err := objAPI.GetBucketInfo(ctx, bucket, BucketOptions{}); err != nil { + writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL) + return + } + + configData, _, err := globalBucketMetadataSys.GetCorsConfigXML(bucket) + if err != nil { + if errors.Is(err, errConfigNotFound) { + writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrNoSuchCORSConfiguration), r.URL) + return + } + writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL) + return + } + + writeSuccessResponseXML(w, configData) +} + +// DeleteBucketCorsHandler - DELETE bucket cors. +func (api objectAPIHandlers) DeleteBucketCorsHandler(w http.ResponseWriter, r *http.Request) { + ctx := newContext(r, w, "DeleteBucketCors") + + defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r)) + + objAPI := api.ObjectAPI() + if objAPI == nil { + writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL) + return + } + + vars := mux.Vars(r) + bucket := vars["bucket"] + + if s3Error := checkRequestAuthType(ctx, r, policy.DeleteBucketCorsAction, bucket, ""); s3Error != ErrNone { + writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL) + return + } + + if _, err := objAPI.GetBucketInfo(ctx, bucket, BucketOptions{}); err != nil { + writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL) + return + } + + updatedAt, err := updateLocalBucketCORSMetadata(ctx, objAPI, bucket, nil) + if err != nil { + writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL) + return + } + + replLogIf(ctx, globalSiteReplicationSys.BucketMetaHook(ctx, madmin.SRBucketMeta{ + Type: madmin.SRBucketMetaTypeCorsConfig, + Bucket: bucket, + Cors: nil, + UpdatedAt: updatedAt, + })) + + writeSuccessNoContent(w) +} diff --git a/cmd/bucket-cors-handlers_test.go b/cmd/bucket-cors-handlers_test.go new file mode 100644 index 000000000..026229f6e --- /dev/null +++ b/cmd/bucket-cors-handlers_test.go @@ -0,0 +1,164 @@ +// Copyright (c) 2015-2021 MinIO, Inc. +// +// This file is part of MinIO Object Storage stack +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package cmd + +import ( + "bytes" + "net/http" + "net/http/httptest" + "testing" + + "github.com/minio/minio/internal/auth" +) + +const testCORSDoc = `http://example.comGETPUTETag3000` + +func TestBucketCorsHandlers(t *testing.T) { + ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{t: t, objAPITest: testBucketCorsHandlers, endpoints: []string{"PutBucketCors", "GetBucketCors", "DeleteBucketCors"}}) +} + +func testBucketCorsHandlers(obj ObjectLayer, instanceType, bucketName string, apiRouter http.Handler, + creds auth.Credentials, t *testing.T, +) { + // PUT + req, err := newTestSignedRequestV4(http.MethodPut, getBucketCorsURL("", bucketName), + int64(len(testCORSDoc)), bytes.NewReader([]byte(testCORSDoc)), creds.AccessKey, creds.SecretKey, nil) + if err != nil { + t.Fatal(err) + } + rec := httptest.NewRecorder() + apiRouter.ServeHTTP(rec, req) + if rec.Code != http.StatusOK { + t.Fatalf("PUT cors: expected 200, got %d: %s", rec.Code, rec.Body.String()) + } + + // GET returns what we stored + req, err = newTestSignedRequestV4(http.MethodGet, getBucketCorsURL("", bucketName), + 0, nil, creds.AccessKey, creds.SecretKey, nil) + if err != nil { + t.Fatal(err) + } + rec = httptest.NewRecorder() + apiRouter.ServeHTTP(rec, req) + if rec.Code != http.StatusOK { + t.Fatalf("GET cors: expected 200, got %d: %s", rec.Code, rec.Body.String()) + } + if !bytes.Contains(rec.Body.Bytes(), []byte("http://example.com")) { + t.Fatalf("GET cors: body missing origin: %s", rec.Body.String()) + } + + // DELETE + req, err = newTestSignedRequestV4(http.MethodDelete, getBucketCorsURL("", bucketName), + 0, nil, creds.AccessKey, creds.SecretKey, nil) + if err != nil { + t.Fatal(err) + } + rec = httptest.NewRecorder() + apiRouter.ServeHTTP(rec, req) + if rec.Code != http.StatusNoContent { + t.Fatalf("DELETE cors: expected 204, got %d", rec.Code) + } + + // GET after delete → 404 NoSuchCORSConfiguration + req, err = newTestSignedRequestV4(http.MethodGet, getBucketCorsURL("", bucketName), + 0, nil, creds.AccessKey, creds.SecretKey, nil) + if err != nil { + t.Fatal(err) + } + rec = httptest.NewRecorder() + apiRouter.ServeHTTP(rec, req) + if rec.Code != http.StatusNotFound { + t.Fatalf("GET cors after delete: expected 404, got %d", rec.Code) + } + + // Malformed XML → 400 + req, err = newTestSignedRequestV4(http.MethodPut, getBucketCorsURL("", bucketName), + int64(len("")), bytes.NewReader([]byte("")), creds.AccessKey, creds.SecretKey, nil) + if err != nil { + t.Fatal(err) + } + rec = httptest.NewRecorder() + apiRouter.ServeHTTP(rec, req) + if rec.Code != http.StatusBadRequest { + t.Fatalf("PUT malformed cors: expected 400, got %d", rec.Code) + } + + // Missing Content-MD5 is rejected before the body is parsed. + req, err = newTestRequest(http.MethodPut, getBucketCorsURL("", bucketName), + int64(len(testCORSDoc)), bytes.NewReader([]byte(testCORSDoc))) + if err != nil { + t.Fatal(err) + } + req.Header.Del("Content-Md5") + if err = signRequestV4(req, creds.AccessKey, creds.SecretKey); err != nil { + t.Fatal(err) + } + rec = httptest.NewRecorder() + apiRouter.ServeHTTP(rec, req) + if rec.Code != http.StatusBadRequest || !bytes.Contains(rec.Body.Bytes(), []byte("MissingContentMD5")) { + t.Fatalf("PUT cors without Content-MD5: expected MissingContentMD5, got %d: %s", rec.Code, rec.Body.String()) + } + + // A signed but incorrect Content-MD5 is rejected while reading the body. + req, err = newTestRequest(http.MethodPut, getBucketCorsURL("", bucketName), + int64(len(testCORSDoc)), bytes.NewReader([]byte(testCORSDoc))) + if err != nil { + t.Fatal(err) + } + req.Header.Set("Content-Md5", getMD5HashBase64([]byte("different body"))) + if err = signRequestV4(req, creds.AccessKey, creds.SecretKey); err != nil { + t.Fatal(err) + } + rec = httptest.NewRecorder() + apiRouter.ServeHTTP(rec, req) + if rec.Code != http.StatusBadRequest || !bytes.Contains(rec.Body.Bytes(), []byte("BadDigest")) { + t.Fatalf("PUT cors with bad Content-MD5: expected BadDigest, got %d: %s", rec.Code, rec.Body.String()) + } + + // Re-PUT the config so the store→GetCorsConfig→enforce seam below has + // something to enforce (the earlier DELETE removed it). + req, err = newTestSignedRequestV4(http.MethodPut, getBucketCorsURL("", bucketName), + int64(len(testCORSDoc)), bytes.NewReader([]byte(testCORSDoc)), creds.AccessKey, creds.SecretKey, nil) + if err != nil { + t.Fatal(err) + } + rec = httptest.NewRecorder() + apiRouter.ServeHTTP(rec, req) + if rec.Code != http.StatusOK { + t.Fatalf("PUT cors (re-put): expected 200, got %d: %s", rec.Code, rec.Body.String()) + } + + // End-to-end enforcement: drive an OPTIONS preflight through the real + // corsHandler wrapper (not applyBucketCors in isolation), exercising the + // full store -> globalBucketMetadataSys.GetCorsConfig -> enforce seam. + wrapped := corsHandler(apiRouter) + + preflightURL := getBucketCorsURL("", bucketName) + preflightReq := httptest.NewRequest(http.MethodOptions, preflightURL, nil) + preflightReq.Header.Set("Origin", "http://example.com") + preflightReq.Header.Set("Access-Control-Request-Method", http.MethodGet) + + rec = httptest.NewRecorder() + wrapped.ServeHTTP(rec, preflightReq) + if rec.Code != http.StatusOK { + t.Fatalf("OPTIONS preflight via corsHandler: expected 200, got %d: %s", rec.Code, rec.Body.String()) + } + if got := rec.Header().Get("Access-Control-Allow-Origin"); got != "http://example.com" { + t.Fatalf("OPTIONS preflight via corsHandler: expected Access-Control-Allow-Origin echoed, got %q", got) + } +} diff --git a/cmd/bucket-cors-middleware_test.go b/cmd/bucket-cors-middleware_test.go new file mode 100644 index 000000000..71af4cf0b --- /dev/null +++ b/cmd/bucket-cors-middleware_test.go @@ -0,0 +1,743 @@ +// Copyright (c) 2015-2021 MinIO, Inc. +// +// This file is part of MinIO Object Storage stack +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package cmd + +import ( + "context" + "fmt" + "net/http" + "net/http/httptest" + "strings" + "sync/atomic" + "testing" + + "github.com/minio/minio/internal/auth" + "github.com/minio/minio/internal/bucket/cors" +) + +type corsLookupCountingObjectLayer struct { + ObjectLayer + getObjectNInfoCalls atomic.Int64 +} + +func (o *corsLookupCountingObjectLayer) GetObjectNInfo(ctx context.Context, bucket, object string, rs *HTTPRangeSpec, h http.Header, opts ObjectOptions) (*GetObjectReader, error) { + o.getObjectNInfoCalls.Add(1) + return o.ObjectLayer.GetObjectNInfo(ctx, bucket, object, rs, h, opts) +} + +func TestPerBucketCorsPreflight(t *testing.T) { + cfg := &cors.Config{CORSRules: []cors.Rule{{ + AllowedOrigins: []string{"http://example.com"}, + AllowedMethods: []string{"GET", "PUT"}, + AllowedHeaders: []string{"*"}, + ExposeHeaders: []string{"ETag"}, + MaxAgeSeconds: 3000, + }}} + + rec := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodOptions, "/mybucket/obj", nil) + req.Header.Set("Origin", "http://example.com") + req.Header.Set("Access-Control-Request-Method", "GET") + req.Header.Set("Access-Control-Request-Headers", "X-Amz-Date") + + handled := applyBucketCors(rec, req, cfg) + if !handled { + t.Fatal("expected preflight to be handled") + } + if got := rec.Header().Get("Access-Control-Allow-Origin"); got != "http://example.com" { + t.Fatalf("allow-origin = %q", got) + } + if got := rec.Header().Get("Access-Control-Allow-Credentials"); got != "true" { + t.Fatalf("allow-credentials = %q", got) + } + if got := rec.Header().Get("Access-Control-Allow-Methods"); got != "GET, PUT" { + t.Fatalf("allow-methods = %q", got) + } + if got := rec.Header().Get("Access-Control-Allow-Headers"); got != "X-Amz-Date" { + t.Fatalf("allow-headers = %q", got) + } + if got := rec.Header().Get("Access-Control-Expose-Headers"); got != "ETag" { + t.Fatalf("expose-headers = %q", got) + } + if got := rec.Header().Get("Access-Control-Max-Age"); got != "3000" { + t.Fatalf("max-age = %q", got) + } + requireCorsVary(t, rec.Header()) + if rec.Code != http.StatusOK { + t.Fatalf("preflight status = %d", rec.Code) + } + requireCorsOriginVary(t, rec.Header()) +} + +func TestPerBucketCorsActualRequestNoMatchVariesByOrigin(t *testing.T) { + cfg := &cors.Config{CORSRules: []cors.Rule{{ + AllowedOrigins: []string{"https://allowed.example.com"}, + AllowedMethods: []string{"GET"}, + }}} + rec := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodGet, "/mybucket/obj", nil) + req.Header.Set("Origin", "https://denied.example.com") + + if handled := applyBucketCors(rec, req, cfg); handled { + t.Fatal("actual request must continue when CORS does not match") + } + if got := rec.Header().Get("Access-Control-Allow-Origin"); got != "" { + t.Fatalf("allow-origin = %q", got) + } + requireCorsOriginVary(t, rec.Header()) +} + +func TestPerBucketCorsPreflightNoMatch(t *testing.T) { + cfg := &cors.Config{CORSRules: []cors.Rule{{ + AllowedOrigins: []string{"http://example.com"}, + AllowedMethods: []string{"GET"}, + }}} + rec := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodOptions, "/mybucket/obj", nil) + req.Header.Set("Origin", "http://evil.com") + req.Header.Set("Access-Control-Request-Method", "GET") + + handled := applyBucketCors(rec, req, cfg) + if !handled { + t.Fatal("expected preflight to be handled (rejected)") + } + if rec.Code != http.StatusForbidden { + t.Fatalf("expected 403 for disallowed origin, got %d", rec.Code) + } + if got := rec.Header().Get("Access-Control-Allow-Origin"); got != "" { + t.Fatalf("rejected preflight returned allow-origin %q", got) + } + requireCorsVary(t, rec.Header()) +} + +func TestPerBucketCorsPreflightWildcardOriginAndZeroMaxAge(t *testing.T) { + doc := `*GETHEAD*ETag0` + cfg, err := cors.ParseBucketCorsConfig(strings.NewReader(doc)) + if err != nil { + t.Fatal(err) + } + + rec := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodOptions, "/mybucket/obj", nil) + req.Header.Set("Origin", "https://app.example.com") + req.Header.Set("Access-Control-Request-Method", "GET") + req.Header.Set("Access-Control-Request-Headers", "RANGE") + + if handled := applyBucketCors(rec, req, cfg); !handled { + t.Fatal("expected preflight to be handled") + } + if got := rec.Header().Get("Access-Control-Allow-Origin"); got != "*" { + t.Fatalf("allow-origin = %q", got) + } + if got := rec.Header().Get("Access-Control-Allow-Credentials"); got != "" { + t.Fatalf("allow-credentials = %q", got) + } + if got := rec.Header().Get("Access-Control-Allow-Methods"); got != "GET, HEAD" { + t.Fatalf("allow-methods = %q", got) + } + if got := rec.Header().Get("Access-Control-Allow-Headers"); got != "RANGE" { + t.Fatalf("allow-headers = %q", got) + } + if got := rec.Header().Get("Access-Control-Expose-Headers"); got != "ETag" { + t.Fatalf("expose-headers = %q", got) + } + if got := rec.Header().Get("Access-Control-Max-Age"); got != "0" { + t.Fatalf("max-age = %q", got) + } + requireCorsVary(t, rec.Header()) +} + +func TestPerBucketCorsPreflightUsesFirstFullyMatchingRule(t *testing.T) { + cfg := &cors.Config{CORSRules: []cors.Rule{ + { + AllowedOrigins: []string{"https://app.example.com"}, + AllowedMethods: []string{"GET"}, + AllowedHeaders: []string{"x-a"}, + ExposeHeaders: []string{"x-rule-a"}, + MaxAgeSeconds: 1, + }, + { + AllowedOrigins: []string{"https://app.example.com"}, + AllowedMethods: []string{"GET", "HEAD"}, + AllowedHeaders: []string{"*"}, + ExposeHeaders: []string{"x-rule-b"}, + MaxAgeSeconds: 2, + }, + }} + rec := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodOptions, "/mybucket/obj", nil) + req.Header.Set("Origin", "https://app.example.com") + req.Header.Set("Access-Control-Request-Method", "GET") + req.Header.Set("Access-Control-Request-Headers", "X-B") + + if handled := applyBucketCors(rec, req, cfg); !handled { + t.Fatal("expected preflight to be handled") + } + if got := rec.Header().Get("Access-Control-Expose-Headers"); got != "x-rule-b" { + t.Fatalf("selected rule expose-headers = %q", got) + } + if got := rec.Header().Get("Access-Control-Allow-Methods"); got != "GET, HEAD" { + t.Fatalf("selected rule allow-methods = %q", got) + } + if got := rec.Header().Get("Access-Control-Max-Age"); got != "2" { + t.Fatalf("selected rule max-age = %q", got) + } +} + +func TestPerBucketCorsActualRequest(t *testing.T) { + cfg := &cors.Config{CORSRules: []cors.Rule{{ + AllowedOrigins: []string{"*"}, + AllowedMethods: []string{"GET"}, + ExposeHeaders: []string{"ETag"}, + }}} + rec := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodGet, "/mybucket/obj", nil) + req.Header.Set("Origin", "http://any.com") + + handled := applyBucketCors(rec, req, cfg) + if handled { + t.Fatal("actual (non-preflight) request must not be terminated by CORS") + } + if got := rec.Header().Get("Access-Control-Allow-Origin"); got != "*" { + t.Fatalf("allow-origin = %q", got) + } + if got := rec.Header().Get("Access-Control-Allow-Credentials"); got != "" { + t.Fatalf("allow-credentials = %q", got) + } + if got := rec.Header().Get("Access-Control-Expose-Headers"); got != "ETag" { + t.Fatalf("expose-headers = %q", got) + } +} + +func TestPerBucketCorsOriginPatternResponse(t *testing.T) { + cfg := &cors.Config{CORSRules: []cors.Rule{{ + AllowedOrigins: []string{"https://app.example.com", "https://*", "*"}, + AllowedMethods: []string{"GET"}, + }}} + + tests := []struct { + origin string + wantOrigin string + wantCredentials string + }{ + {"https://app.example.com", "https://app.example.com", "true"}, + {"https://other.example.com", "https://other.example.com", "true"}, + {"http://other.example.com", "*", ""}, + } + + for _, tt := range tests { + rec := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodGet, "/mybucket/obj", nil) + req.Header.Set("Origin", tt.origin) + if handled := applyBucketCors(rec, req, cfg); handled { + t.Fatal("actual request must not be terminated by CORS") + } + if got := rec.Header().Get("Access-Control-Allow-Origin"); got != tt.wantOrigin { + t.Fatalf("origin %q: allow-origin = %q, want %q", tt.origin, got, tt.wantOrigin) + } + if got := rec.Header().Get("Access-Control-Allow-Credentials"); got != tt.wantCredentials { + t.Fatalf("origin %q: allow-credentials = %q, want %q", tt.origin, got, tt.wantCredentials) + } + } +} + +func TestBucketCorsMetadataErrorFailsClosed(t *testing.T) { + oldObjectAPI := newObjectLayerFn() + 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 + }() + + wrapped := corsHandler(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusNoContent) + })) + + for _, method := range []string{http.MethodGet, http.MethodOptions} { + rec := httptest.NewRecorder() + req := httptest.NewRequest(method, getGetObjectURL("", "cors-metadata-error", "object"), nil) + req.Header.Set("Origin", "https://app.example.com") + if method == http.MethodOptions { + req.Header.Set("Access-Control-Request-Method", http.MethodGet) + } + wrapped.ServeHTTP(rec, req) + + if rec.Code != http.StatusNoContent { + t.Fatalf("%s status = %d, want %d", method, rec.Code, http.StatusNoContent) + } + if got := rec.Header().Get("Access-Control-Allow-Origin"); got != "" { + t.Fatalf("%s metadata error fell back to global allow-origin %q", method, got) + } + if got := rec.Header().Get("Access-Control-Allow-Credentials"); got != "" { + t.Fatalf("%s metadata error fell back to global credentials %q", method, got) + } + } +} + +func TestBucketCorsSkipsMetadataLookupWithoutOrigin(t *testing.T) { + ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{ + t: t, + objAPITest: testBucketCorsSkipsMetadataLookupWithoutOrigin, + endpoints: []string{"GetObject"}, + }) +} + +func testBucketCorsSkipsMetadataLookupWithoutOrigin(obj ObjectLayer, _ string, _ string, _ http.Handler, _ auth.Credentials, t *testing.T) { + oldObjectAPI := newObjectLayerFn() + counting := &corsLookupCountingObjectLayer{ObjectLayer: obj} + setObjectLayer(counting) + defer setObjectLayer(oldObjectAPI) + + wrapped := corsHandler(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusNoContent) + })) + rec := httptest.NewRecorder() + wrapped.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, getGetObjectURL("", "api", "v1/login"), nil)) + + if rec.Code != http.StatusNoContent { + t.Fatalf("status = %d, want %d", rec.Code, http.StatusNoContent) + } + requireCorsOriginVary(t, rec.Header()) + if got := counting.getObjectNInfoCalls.Load(); got != 0 { + t.Fatalf("request without Origin performed %d bucket metadata reads", got) + } +} + +func TestBucketCorsOriginlessPreflightShapeUsesGlobalHandler(t *testing.T) { + nextCalled := false + wrapped := corsHandler(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + nextCalled = true + w.WriteHeader(http.StatusTeapot) + })) + rec := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodOptions, getGetObjectURL("", "api", "v1/login"), nil) + req.Header.Set("Access-Control-Request-Method", http.MethodGet) + wrapped.ServeHTTP(rec, req) + + if rec.Code != http.StatusNoContent { + t.Fatalf("status = %d, want %d", rec.Code, http.StatusNoContent) + } + if nextCalled { + t.Fatal("originless preflight-shaped OPTIONS reached the application handler") + } + requireCorsOriginVary(t, rec.Header()) +} + +func TestBucketCorsNoConfigUsesGlobalFallback(t *testing.T) { + ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{ + t: t, + objAPITest: testBucketCorsNoConfigUsesGlobalFallback, + endpoints: []string{"GetBucketCors"}, + }) +} + +func TestBucketCorsMissingBucketUsesGlobalFallback(t *testing.T) { + ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{ + t: t, + objAPITest: testBucketCorsMissingBucketUsesGlobalFallback, + endpoints: []string{"GetBucketCors"}, + }) +} + +func testBucketCorsMissingBucketUsesGlobalFallback(obj ObjectLayer, _ string, bucket string, _ http.Handler, _ auth.Credentials, t *testing.T) { + // Model a fully started server: bucket metadata loading has completed, so + // a name that is not resident is genuinely not a CORS-bearing bucket. + restore := markBucketMetadataInitialized(t) + defer restore() + + // A non-resident bucket name must not cause any bucket-metadata disk read. + oldObjectAPI := newObjectLayerFn() + counting := &corsLookupCountingObjectLayer{ObjectLayer: obj} + setObjectLayer(counting) + defer setObjectLayer(oldObjectAPI) + + before := bucketMetadataMapLen() + + wrapped := corsHandler(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusNotFound) + })) + rec := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodGet, getGetObjectURL("", bucket+"-missing", "object"), nil) + req.Header.Set("Origin", "https://app.example.com") + wrapped.ServeHTTP(rec, req) + + if rec.Code != http.StatusNotFound { + t.Fatalf("status = %d, want %d", rec.Code, http.StatusNotFound) + } + if got := rec.Header().Get("Access-Control-Allow-Origin"); got != "https://app.example.com" { + t.Fatalf("allow-origin = %q", got) + } + if got := rec.Header().Get("Access-Control-Allow-Credentials"); got != "true" { + t.Fatalf("allow-credentials = %q", got) + } + // Regression guard: the pre-auth CORS lookup for a non-existent bucket must + // neither read bucket metadata from disk nor cache a synthetic entry. + if got := counting.getObjectNInfoCalls.Load(); got != 0 { + t.Fatalf("missing-bucket CORS lookup performed %d bucket metadata reads", got) + } + if after := bucketMetadataMapLen(); after != before { + t.Fatalf("missing-bucket CORS lookup grew metadataMap from %d to %d", before, after) + } +} + +func testBucketCorsNoConfigUsesGlobalFallback(_ ObjectLayer, _ string, bucket string, _ http.Handler, _ auth.Credentials, t *testing.T) { + wrapped := corsHandler(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusNoContent) + })) + rec := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodGet, getGetObjectURL("", bucket, "object"), nil) + req.Header.Set("Origin", "https://app.example.com") + wrapped.ServeHTTP(rec, req) + + if rec.Code != http.StatusNoContent { + t.Fatalf("status = %d, want %d", rec.Code, http.StatusNoContent) + } + if got := rec.Header().Get("Access-Control-Allow-Origin"); got != "https://app.example.com" { + t.Fatalf("allow-origin = %q", got) + } + if got := rec.Header().Get("Access-Control-Allow-Credentials"); got != "true" { + t.Fatalf("allow-credentials = %q", got) + } +} + +func TestPerBucketCorsActualPatternOriginSupportsCredentials(t *testing.T) { + cfg := &cors.Config{CORSRules: []cors.Rule{{ + AllowedOrigins: []string{"https://*.example.com"}, + AllowedMethods: []string{"GET"}, + }}} + rec := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodGet, "/mybucket/obj", nil) + req.Header.Set("Origin", "https://app.example.com") + + if handled := applyBucketCors(rec, req, cfg); handled { + t.Fatal("actual request must continue") + } + if got := rec.Header().Get("Access-Control-Allow-Origin"); got != "https://app.example.com" { + t.Fatalf("allow-origin = %q", got) + } + if got := rec.Header().Get("Access-Control-Allow-Credentials"); got != "true" { + t.Fatalf("allow-credentials = %q", got) + } +} + +func TestPerBucketCorsActualNullOriginSurvivesForwardingMiddleware(t *testing.T) { + next := setBucketForwardingMiddleware(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusNoContent) + })) + + t.Run("per-bucket null origin", func(t *testing.T) { + cfg := &cors.Config{CORSRules: []cors.Rule{{ + AllowedOrigins: []string{"null"}, + AllowedMethods: []string{"GET"}, + }}} + rec := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodGet, "/mybucket/obj", nil) + req.Header.Set("Origin", "null") + + if handled := applyBucketCors(rec, req, cfg); handled { + t.Fatal("actual request must continue") + } + next.ServeHTTP(rec, req) + + if got := rec.Header().Get("Access-Control-Allow-Origin"); got != "null" { + t.Fatalf("allow-origin = %q", got) + } + if got := rec.Header().Get("Access-Control-Allow-Credentials"); got != "true" { + t.Fatalf("allow-credentials = %q", got) + } + }) + + t.Run("legacy unmarked null origin", func(t *testing.T) { + rec := httptest.NewRecorder() + rec.Header().Set("Access-Control-Allow-Origin", "null") + req := httptest.NewRequest(http.MethodGet, "/mybucket/obj", nil) + + next.ServeHTTP(rec, req) + + if got := rec.Header().Get("Access-Control-Allow-Origin"); got != "*" { + t.Fatalf("allow-origin = %q", got) + } + }) +} + +func requireCorsVary(t *testing.T, header http.Header) { + t.Helper() + values := strings.Join(header.Values("Vary"), ",") + for _, want := range []string{"Origin", "Access-Control-Request-Method", "Access-Control-Request-Headers"} { + if !strings.Contains(values, want) { + t.Fatalf("Vary = %q, missing %q", values, want) + } + } +} + +func requireCorsOriginVary(t *testing.T, header http.Header) { + t.Helper() + if values := strings.Join(header.Values("Vary"), ","); !strings.Contains(values, "Origin") { + t.Fatalf("Vary = %q, missing Origin", values) + } +} + +// markBucketMetadataInitialized marks the global bucket-metadata subsystem as +// fully loaded, modeling a running server (the API test harness sets up the +// subsystem but does not run Init). It returns a function that restores the +// previous state. +func markBucketMetadataInitialized(t *testing.T) func() { + t.Helper() + sys := globalBucketMetadataSys + if sys == nil { + t.Fatal("globalBucketMetadataSys is nil") + } + sys.Lock() + prev := sys.initialized + sys.initialized = true + sys.Unlock() + return func() { + sys.Lock() + sys.initialized = prev + sys.Unlock() + } +} + +// bucketMetadataMapLen returns the number of resident bucket-metadata entries. +func bucketMetadataMapLen() int { + sys := globalBucketMetadataSys + if sys == nil { + return 0 + } + sys.RLock() + defer sys.RUnlock() + return len(sys.metadataMap) +} + +// TestBucketCorsUnknownBucketDoesNotGrowMetadata is the regression guard for +// the pre-auth resource-exhaustion path: an unauthenticated, Origin-bearing +// request whose first path segment is not a real bucket must fall back to the +// global CORS policy without loading bucket metadata from disk and without +// caching a synthetic entry. Before the resident-only lookup, each distinct +// name grew metadataMap by one and issued an erasure metadata probe. +func TestBucketCorsUnknownBucketDoesNotGrowMetadata(t *testing.T) { + ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{ + t: t, + objAPITest: testBucketCorsUnknownBucketDoesNotGrowMetadata, + endpoints: []string{"GetBucketCors"}, + }) +} + +func testBucketCorsUnknownBucketDoesNotGrowMetadata(obj ObjectLayer, _ string, _ string, _ http.Handler, _ auth.Credentials, t *testing.T) { + restore := markBucketMetadataInitialized(t) + defer restore() + + oldObjectAPI := newObjectLayerFn() + counting := &corsLookupCountingObjectLayer{ObjectLayer: obj} + setObjectLayer(counting) + defer setObjectLayer(oldObjectAPI) + + wrapped := corsHandler(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusNotFound) + })) + + before := bucketMetadataMapLen() + // Console/admin routes plus enough distinct valid names to make accidental + // cache growth or one metadata probe per name unambiguous. + names := []string{"minio", "api"} + for i := 0; i < 500; i++ { + names = append(names, fmt.Sprintf("cors-missing-%03d", i)) + } + for _, name := range names { + for _, method := range []string{http.MethodGet, http.MethodOptions} { + rec := httptest.NewRecorder() + req := httptest.NewRequest(method, getGetObjectURL("", name, "obj"), nil) + req.Header.Set("Origin", "https://app.example.com") + if method == http.MethodOptions { + req.Header.Set("Access-Control-Request-Method", http.MethodGet) + } + wrapped.ServeHTTP(rec, req) + + if got := rec.Header().Get("Access-Control-Allow-Origin"); got != "https://app.example.com" { + t.Fatalf("%s/%s: allow-origin = %q, want global fallback", name, method, got) + } + } + } + for _, path := range []string{"/../obj", "/A/obj", "/x/obj", "/minio/admin/v3/info", "/api/v1/login"} { + rec := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodGet, path, nil) + req.Header.Set("Origin", "https://app.example.com") + wrapped.ServeHTTP(rec, req) + if got := rec.Header().Get("Access-Control-Allow-Origin"); got != "https://app.example.com" { + t.Fatalf("%s: allow-origin = %q, want global fallback", path, got) + } + } + + if got := counting.getObjectNInfoCalls.Load(); got != 0 { + t.Fatalf("unknown-bucket CORS lookups performed %d bucket metadata reads", got) + } + if after := bucketMetadataMapLen(); after != before { + t.Fatalf("unknown-bucket CORS lookups grew metadataMap from %d to %d", before, after) + } +} + +func TestBucketCorsStartupMissFailsClosedWithoutIO(t *testing.T) { + ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{ + t: t, + objAPITest: testBucketCorsStartupMissFailsClosedWithoutIO, + endpoints: []string{"GetBucketCors"}, + }) +} + +func testBucketCorsStartupMissFailsClosedWithoutIO(obj ObjectLayer, _ string, _ string, _ http.Handler, _ auth.Credentials, t *testing.T) { + oldObjectAPI := newObjectLayerFn() + oldMetadataSys := globalBucketMetadataSys + counting := &corsLookupCountingObjectLayer{ObjectLayer: obj} + setObjectLayer(counting) + globalBucketMetadataSys = NewBucketMetadataSys() + defer func() { + setObjectLayer(oldObjectAPI) + globalBucketMetadataSys = oldMetadataSys + }() + + innerCalled := false + wrapped := corsHandler(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + innerCalled = true + w.WriteHeader(http.StatusNoContent) + })) + rec := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodGet, "/startup-missing/object", nil) + req.Header.Set("Origin", "https://app.example.com") + wrapped.ServeHTTP(rec, req) + + if !innerCalled || rec.Code != http.StatusNoContent { + t.Fatalf("startup miss did not reach inner handler: called=%v status=%d", innerCalled, rec.Code) + } + if got := rec.Header().Get("Access-Control-Allow-Origin"); got != "" { + t.Fatalf("startup miss used permissive global CORS: %q", got) + } + if got := counting.getObjectNInfoCalls.Load(); got != 0 { + t.Fatalf("startup miss performed %d metadata reads", got) + } + if got := globalBucketMetadataSys.Count(); got != 0 { + t.Fatalf("startup miss grew metadataMap to %d", got) + } +} + +// markBucketMetadataLoadFailed records a bucket as one whose metadata failed to +// load at startup while the subsystem is Initialized, modeling the degraded +// state where a real bucket is not resident. Returns a restore function. +func markBucketMetadataLoadFailed(t *testing.T, bucket string) func() { + t.Helper() + sys := globalBucketMetadataSys + if sys == nil { + t.Fatal("globalBucketMetadataSys is nil") + } + sys.Lock() + _, had := sys.loadFailed[bucket] + sys.loadFailed[bucket] = struct{}{} + sys.Unlock() + return func() { + sys.Lock() + if !had { + delete(sys.loadFailed, bucket) + } + sys.Unlock() + } +} + +// TestBucketCorsLoadFailedBucketFailsClosed guards P1: a real bucket whose +// metadata could not be loaded at startup (present in loadFailed, subsystem +// Initialized) must NOT be answered with the permissive global CORS policy. We +// cannot rule out a restrictive per-bucket config for it, so it must fail +// closed — without a synchronous disk read. +func TestBucketCorsLoadFailedBucketFailsClosed(t *testing.T) { + ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{ + t: t, + objAPITest: testBucketCorsLoadFailedBucketFailsClosed, + endpoints: []string{"GetBucketCors"}, + }) +} + +func testBucketCorsLoadFailedBucketFailsClosed(obj ObjectLayer, _ string, _ string, _ http.Handler, _ auth.Credentials, t *testing.T) { + restoreInit := markBucketMetadataInitialized(t) + defer restoreInit() + restoreFail := markBucketMetadataLoadFailed(t, "strict-cors-bucket") + defer restoreFail() + + oldObjectAPI := newObjectLayerFn() + counting := &corsLookupCountingObjectLayer{ObjectLayer: obj} + setObjectLayer(counting) + defer setObjectLayer(oldObjectAPI) + + wrapped := corsHandler(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusNotFound) + })) + + for _, method := range []string{http.MethodGet, http.MethodOptions} { + rec := httptest.NewRecorder() + req := httptest.NewRequest(method, getGetObjectURL("", "strict-cors-bucket", "object"), nil) + req.Header.Set("Origin", "https://app.example.com") + if method == http.MethodOptions { + req.Header.Set("Access-Control-Request-Method", http.MethodGet) + } + wrapped.ServeHTTP(rec, req) + + if got := rec.Header().Get("Access-Control-Allow-Origin"); got != "" { + t.Fatalf("%s: load-failed bucket fell back to global allow-origin %q", method, got) + } + if got := rec.Header().Get("Access-Control-Allow-Credentials"); got != "" { + t.Fatalf("%s: load-failed bucket fell back to global credentials %q", method, got) + } + } + if got := counting.getObjectNInfoCalls.Load(); got != 0 { + t.Fatalf("load-failed CORS lookup performed %d synchronous bucket metadata reads", got) + } +} + +// TestBucketCorsResidentConfigSurvivesRefreshFailure: a resident bucket keeps +// its last loaded CORS configuration through a failed refresh, like every +// other bucket configuration, and the failure set never records a resident +// bucket. Only a bucket that was never loaded fails closed. +func TestBucketCorsResidentConfigSurvivesRefreshFailure(t *testing.T) { + ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{ + t: t, + objAPITest: testBucketCorsResidentConfigSurvivesRefreshFailure, + endpoints: []string{"GetBucketCors"}, + }) +} + +func testBucketCorsResidentConfigSurvivesRefreshFailure(obj ObjectLayer, _ string, bucket string, _ http.Handler, _ auth.Credentials, t *testing.T) { + if _, err := updateLocalBucketCORSMetadata(t.Context(), obj, bucket, []byte(testSiteReplicationCORSDoc)); err != nil { + t.Fatal(err) + } + sys := globalBucketMetadataSys + sys.Lock() + sys.noteLoadFailure(bucket) + _, marked := sys.loadFailed[bucket] + sys.Unlock() + if marked { + t.Fatal("a resident bucket was recorded as a load failure") + } + cfg, _, err := sys.GetResidentCorsConfig(bucket) + if err != nil || cfg == nil { + t.Fatalf("resident CORS configuration lost after a refresh failure: cfg=%v err=%v", cfg, err) + } +} diff --git a/cmd/bucket-cors-site-replication_test.go b/cmd/bucket-cors-site-replication_test.go new file mode 100644 index 000000000..64c37330b --- /dev/null +++ b/cmd/bucket-cors-site-replication_test.go @@ -0,0 +1,1295 @@ +// Copyright (c) 2015-2021 MinIO, Inc. +// +// This file is part of MinIO Object Storage stack +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package cmd + +import ( + "bytes" + "encoding/base64" + "encoding/binary" + "encoding/json" + "errors" + "net/http" + "net/http/httptest" + "sync" + "testing" + "time" + + "github.com/minio/madmin-go/v3" + "github.com/minio/minio/internal/auth" + "github.com/minio/mux" +) + +const testSiteReplicationCORSDoc = `https://app.example.comGET` + +const testSiteReplicationAlternateCORSDoc = `https://admin.example.comPUT` + +func TestPeerBucketCorsReplicationOrdering(t *testing.T) { + ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{ + t: t, + objAPITest: testPeerBucketCorsReplicationOrdering, + endpoints: []string{"GetBucketCors"}, + }) +} + +func testPeerBucketCorsReplicationOrdering(_ ObjectLayer, _ string, bucket string, _ http.Handler, _ auth.Credentials, t *testing.T) { + ctx := t.Context() + encoded := base64.StdEncoding.EncodeToString([]byte(testSiteReplicationCORSDoc)) + initialMeta, err := globalBucketMetadataSys.Get(bucket) + if err != nil { + t.Fatal(err) + } + putAt := initialMeta.Created.Add(time.Second) + deleteAt := putAt.Add(time.Second) + + // Use a JSON-decoded event for the first apply so the wire representation + // and the real metadata apply path meet in one regression test. + wireData, err := json.Marshal(madmin.SRBucketMeta{ + Type: madmin.SRBucketMetaTypeCorsConfig, + Bucket: bucket, + Cors: &encoded, + UpdatedAt: putAt, + }) + if err != nil { + t.Fatal(err) + } + var item madmin.SRBucketMeta + if err = json.Unmarshal(wireData, &item); err != nil { + t.Fatal(err) + } + if err = globalSiteReplicationSys.PeerBucketCorsConfigHandler(ctx, item.Bucket, item.Cors, item.UpdatedAt); err != nil { + t.Fatalf("peer PUT failed: %v", err) + } + + meta, err := globalBucketMetadataSys.Get(bucket) + if err != nil { + t.Fatal(err) + } + if string(meta.CorsConfigXML) != testSiteReplicationCORSDoc { + t.Fatalf("peer PUT stored %q, want %q", meta.CorsConfigXML, testSiteReplicationCORSDoc) + } + if !meta.CorsConfigUpdatedAt.Equal(putAt) { + t.Fatalf("peer PUT timestamp = %v, want source time %v", meta.CorsConfigUpdatedAt, putAt) + } + cfg, cfgAt, err := globalBucketMetadataSys.GetResidentCorsConfig(bucket) + if err != nil { + t.Fatalf("peer PUT stored raw XML but no parsed config: %v", err) + } + if cfg == nil || !cfgAt.Equal(putAt) { + t.Fatalf("peer PUT parsed config = %#v at %v, want config at %v", cfg, cfgAt, putAt) + } + if _, _, ok := cfg.MatchRule("https://app.example.com", http.MethodGet); !ok { + t.Fatal("peer PUT parsed config does not enforce its origin and method") + } + + if err = globalSiteReplicationSys.PeerBucketCorsConfigHandler(ctx, bucket, nil, deleteAt); err != nil { + t.Fatalf("newer peer DELETE failed: %v", err) + } + meta, err = globalBucketMetadataSys.Get(bucket) + if err != nil { + t.Fatal(err) + } + if len(meta.CorsConfigXML) != 0 || meta.corsConfig != nil { + t.Fatalf("newer peer DELETE left a live config: %q", meta.CorsConfigXML) + } + if !meta.CorsConfigUpdatedAt.Equal(deleteAt) { + t.Fatalf("peer DELETE timestamp = %v, want source time %v", meta.CorsConfigUpdatedAt, deleteAt) + } + + // A delayed older PUT must not resurrect the newer deletion tombstone. + if err = globalSiteReplicationSys.PeerBucketCorsConfigHandler(ctx, bucket, &encoded, putAt); err != nil { + t.Fatalf("stale peer PUT failed: %v", err) + } + meta, err = globalBucketMetadataSys.Get(bucket) + if err != nil { + t.Fatal(err) + } + if len(meta.CorsConfigXML) != 0 || !meta.CorsConfigUpdatedAt.Equal(deleteAt) { + t.Fatalf("stale peer PUT changed tombstone: xml=%q timestamp=%v", meta.CorsConfigXML, meta.CorsConfigUpdatedAt) + } + + // Duplicate delivery at the same timestamp is idempotent. + if err = globalSiteReplicationSys.PeerBucketCorsConfigHandler(ctx, bucket, nil, deleteAt); err != nil { + t.Fatalf("duplicate peer DELETE failed: %v", err) + } + meta, err = globalBucketMetadataSys.Get(bucket) + if err != nil { + t.Fatal(err) + } + if len(meta.CorsConfigXML) != 0 || !meta.CorsConfigUpdatedAt.Equal(deleteAt) { + t.Fatalf("duplicate peer DELETE changed tombstone: xml=%q timestamp=%v", meta.CorsConfigXML, meta.CorsConfigUpdatedAt) + } + + // DELETE wins a same-timestamp conflict deterministically. + if err = globalSiteReplicationSys.PeerBucketCorsConfigHandler(ctx, bucket, &encoded, deleteAt); err != nil { + t.Fatalf("same-timestamp peer PUT failed: %v", err) + } + meta, err = globalBucketMetadataSys.Get(bucket) + if err != nil { + t.Fatal(err) + } + if len(meta.CorsConfigXML) != 0 || !meta.CorsConfigUpdatedAt.Equal(deleteAt) { + t.Fatalf("same-timestamp peer PUT replaced tombstone: xml=%q timestamp=%v", meta.CorsConfigXML, meta.CorsConfigUpdatedAt) + } + + missingBucket := bucket + "-missing" + if err = globalSiteReplicationSys.PeerBucketCorsConfigHandler(ctx, missingBucket, &encoded, UTCNow()); err == nil { + t.Fatal("peer CORS event for missing bucket metadata unexpectedly succeeded") + } + if _, err = globalBucketMetadataSys.Get(missingBucket); !errors.Is(err, errConfigNotFound) { + t.Fatalf("peer CORS event created metadata for missing bucket: %v", err) + } +} + +func TestSiteReplicationMetaInfoPreservesCorsTombstone(t *testing.T) { + ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{ + t: t, + objAPITest: testSiteReplicationMetaInfoPreservesCorsTombstone, + endpoints: []string{"GetBucketCors"}, + }) +} + +func testSiteReplicationMetaInfoPreservesCorsTombstone(obj ObjectLayer, _ string, bucket string, _ http.Handler, _ auth.Credentials, t *testing.T) { + ctx := t.Context() + if _, err := updateLocalBucketCORSMetadata(ctx, obj, bucket, []byte(testSiteReplicationCORSDoc)); err != nil { + t.Fatal(err) + } + deleteAt, err := updateLocalBucketCORSMetadata(ctx, obj, bucket, nil) + if err != nil { + t.Fatal(err) + } + + globalSiteReplicationSys.Lock() + wasEnabled := globalSiteReplicationSys.enabled + globalSiteReplicationSys.enabled = true + globalSiteReplicationSys.Unlock() + defer func() { + globalSiteReplicationSys.Lock() + globalSiteReplicationSys.enabled = wasEnabled + globalSiteReplicationSys.Unlock() + }() + + info, err := globalSiteReplicationSys.SiteReplicationMetaInfo(ctx, obj, madmin.SRStatusOptions{Buckets: true}) + if err != nil { + t.Fatal(err) + } + got := info.Buckets[bucket] + if got.CorsConfig != nil { + t.Fatalf("deleted CORS config reported live payload %q", *got.CorsConfig) + } + if !got.CorsConfigUpdatedAt.Equal(deleteAt) { + t.Fatalf("reported tombstone timestamp = %v, want %v", got.CorsConfigUpdatedAt, deleteAt) + } + + // Model metadata written before the CORS timestamp field existed. + meta, err := readBucketMetadata(ctx, obj, bucket) + if err != nil { + t.Fatal(err) + } + meta.CorsConfigXML = nil + meta.CorsConfigUpdatedAt = time.Time{} + if err = globalBucketMetadataSys.save(ctx, meta); err != nil { + t.Fatal(err) + } + + info, err = globalSiteReplicationSys.SiteReplicationMetaInfo(ctx, obj, madmin.SRStatusOptions{Buckets: true}) + if err != nil { + t.Fatal(err) + } + got = info.Buckets[bucket] + if !got.CorsConfigUpdatedAt.IsZero() { + t.Fatalf("never-configured CORS timestamp = %v, want zero baseline", got.CorsConfigUpdatedAt) + } +} + +func TestHealCorsMetadataPrefersNewerTombstone(t *testing.T) { + ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{ + t: t, + objAPITest: testHealCorsMetadataPrefersNewerTombstone, + endpoints: []string{"GetBucketCors"}, + }) +} + +func TestLatestCORSConfigIgnoresBaseline(t *testing.T) { + created := UTCNow().Add(-time.Hour) + configuredAt := created.Add(time.Minute) + laterCreated := configuredAt.Add(time.Minute) + encoded := base64.StdEncoding.EncodeToString([]byte(testSiteReplicationCORSDoc)) + + bs := map[string]srBucketStatsSummary{ + "configured": { + meta: srBucketMetaInfo{SRBucketInfo: madmin.SRBucketInfo{ + CorsConfig: &encoded, + CorsConfigUpdatedAt: configuredAt, + CreatedAt: created, + }}, + }, + "never-configured": { + meta: srBucketMetaInfo{SRBucketInfo: madmin.SRBucketInfo{ + CorsConfig: nil, + CorsConfigUpdatedAt: time.Time{}, + CreatedAt: laterCreated, + }}, + }, + } + + latestID, latest, ok := latestCORSConfig(bs) + if !ok { + t.Fatal("expected live config to be selected") + } + latestConfig := latest.encodedPayload() + if latestID != "configured" || !latest.updatedAt.Equal(configuredAt) || latestConfig == nil || *latestConfig != encoded { + t.Fatalf("latest = (%q, %v, %v), want configured live config at %v", latestID, latest.updatedAt, latestConfig, configuredAt) + } +} + +func testHealCorsMetadataPrefersNewerTombstone(obj ObjectLayer, _ string, bucket string, _ http.Handler, _ auth.Credentials, t *testing.T) { + ctx := t.Context() + meta, err := readBucketMetadata(ctx, obj, bucket) + if err != nil { + t.Fatal(err) + } + oldAt := meta.Created.Add(time.Second) + deleteAt := oldAt.Add(time.Second) + meta.CorsConfigXML = []byte(testSiteReplicationCORSDoc) + meta.CorsConfigUpdatedAt = oldAt + if err = globalBucketMetadataSys.save(ctx, meta); err != nil { + t.Fatal(err) + } + + localID := globalDeploymentID() + remoteID := "remote-cors-tombstone" + encoded := base64.StdEncoding.EncodeToString([]byte(testSiteReplicationCORSDoc)) + info := srStatusInfo{ + Sites: map[string]madmin.PeerInfo{ + localID: {Name: "local", DeploymentID: localID}, + remoteID: {Name: "remote", DeploymentID: remoteID}, + }, + BucketStats: map[string]map[string]srBucketStatsSummary{ + bucket: { + localID: { + SRBucketStatsSummary: madmin.SRBucketStatsSummary{CorsCfgMismatch: true}, + meta: srBucketMetaInfo{ + SRBucketInfo: madmin.SRBucketInfo{ + Bucket: bucket, + CorsConfig: &encoded, + CorsConfigUpdatedAt: oldAt, + CreatedAt: meta.Created, + }, + DeploymentID: localID, + }, + }, + remoteID: { + SRBucketStatsSummary: madmin.SRBucketStatsSummary{CorsCfgMismatch: true}, + meta: srBucketMetaInfo{ + SRBucketInfo: madmin.SRBucketInfo{ + Bucket: bucket, + CorsConfig: nil, + CorsConfigUpdatedAt: deleteAt, + CreatedAt: meta.Created, + }, + DeploymentID: remoteID, + }, + }, + }, + }, + } + + globalSiteReplicationSys.Lock() + wasEnabled := globalSiteReplicationSys.enabled + globalSiteReplicationSys.enabled = true + globalSiteReplicationSys.Unlock() + defer func() { + globalSiteReplicationSys.Lock() + globalSiteReplicationSys.enabled = wasEnabled + globalSiteReplicationSys.Unlock() + }() + + if err = globalSiteReplicationSys.healCORSMetadata(ctx, obj, bucket, info); err != nil { + t.Fatal(err) + } + meta, err = globalBucketMetadataSys.Get(bucket) + if err != nil { + t.Fatal(err) + } + if len(meta.CorsConfigXML) != 0 || meta.corsConfig != nil { + t.Fatalf("heal retained stale CORS config %q", meta.CorsConfigXML) + } + if !meta.CorsConfigUpdatedAt.Equal(deleteAt) { + t.Fatalf("heal tombstone timestamp = %v, want %v", meta.CorsConfigUpdatedAt, deleteAt) + } +} + +func TestPeerBucketCorsEqualTimestampOrderIndependent(t *testing.T) { + ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{ + t: t, + objAPITest: testPeerBucketCorsEqualTimestampOrderIndependent, + endpoints: []string{"GetBucketCors"}, + }) +} + +func testPeerBucketCorsEqualTimestampOrderIndependent(obj ObjectLayer, _ string, bucket string, _ http.Handler, _ auth.Credentials, t *testing.T) { + ctx := t.Context() + meta, err := readBucketMetadata(ctx, obj, bucket) + if err != nil { + t.Fatal(err) + } + at := meta.Created.Add(time.Second) + first := base64.StdEncoding.EncodeToString([]byte(testSiteReplicationCORSDoc)) + second := base64.StdEncoding.EncodeToString([]byte(testSiteReplicationAlternateCORSDoc)) + + reset := func() { + t.Helper() + meta.CorsConfigXML = nil + meta.CorsConfigUpdatedAt = meta.Created + if err := globalBucketMetadataSys.save(ctx, meta); err != nil { + t.Fatal(err) + } + } + apply := func(encoded *string) { + t.Helper() + if err := globalSiteReplicationSys.PeerBucketCorsConfigHandler(ctx, bucket, encoded, at); err != nil { + t.Fatal(err) + } + } + readPayload := func() string { + t.Helper() + got, err := globalBucketMetadataSys.Get(bucket) + if err != nil { + t.Fatal(err) + } + return string(got.CorsConfigXML) + } + + reset() + apply(&first) + apply(&second) + forward := readPayload() + + reset() + apply(&second) + apply(&first) + reverse := readPayload() + + if forward != reverse { + t.Fatalf("equal-timestamp result depends on arrival order: forward=%q reverse=%q", forward, reverse) + } + want := testSiteReplicationCORSDoc + if bytes.Compare([]byte(testSiteReplicationAlternateCORSDoc), []byte(want)) > 0 { + want = testSiteReplicationAlternateCORSDoc + } + if forward != want { + t.Fatalf("equal-timestamp live winner = %q, want lexicographic maximum %q", forward, want) + } +} + +func TestAdversarialHealCorsPropagatesNewerEqualValueTimestamp(t *testing.T) { + ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{ + t: t, + objAPITest: testAdversarialHealCorsPropagatesNewerEqualValueTimestamp, + endpoints: []string{"GetBucketCors"}, + }) +} + +func testAdversarialHealCorsPropagatesNewerEqualValueTimestamp(obj ObjectLayer, _ string, bucket string, _ http.Handler, _ auth.Credentials, t *testing.T) { + ctx := t.Context() + meta, err := readBucketMetadata(ctx, obj, bucket) + if err != nil { + t.Fatal(err) + } + older := meta.Created.Add(time.Second) + newer := older.Add(time.Second) + meta.CorsConfigXML = []byte(testSiteReplicationCORSDoc) + meta.CorsConfigUpdatedAt = older + if err = globalBucketMetadataSys.save(ctx, meta); err != nil { + t.Fatal(err) + } + + localID := globalDeploymentID() + remoteID := "remote-cors-newer-barrier" + encoded := base64.StdEncoding.EncodeToString([]byte(testSiteReplicationCORSDoc)) + info := srStatusInfo{ + Sites: map[string]madmin.PeerInfo{ + localID: {Name: "local", DeploymentID: localID}, + remoteID: {Name: "remote", DeploymentID: remoteID}, + }, + BucketStats: map[string]map[string]srBucketStatsSummary{ + bucket: { + localID: { + SRBucketStatsSummary: madmin.SRBucketStatsSummary{CorsCfgMismatch: true}, + meta: srBucketMetaInfo{SRBucketInfo: madmin.SRBucketInfo{ + Bucket: bucket, CorsConfig: &encoded, CorsConfigUpdatedAt: older, CreatedAt: meta.Created, + }, DeploymentID: localID}, + }, + remoteID: { + SRBucketStatsSummary: madmin.SRBucketStatsSummary{CorsCfgMismatch: true}, + meta: srBucketMetaInfo{SRBucketInfo: madmin.SRBucketInfo{ + Bucket: bucket, CorsConfig: &encoded, CorsConfigUpdatedAt: newer, CreatedAt: meta.Created, + }, DeploymentID: remoteID}, + }, + }, + }, + } + + globalSiteReplicationSys.Lock() + wasEnabled := globalSiteReplicationSys.enabled + globalSiteReplicationSys.enabled = true + globalSiteReplicationSys.Unlock() + defer func() { + globalSiteReplicationSys.Lock() + globalSiteReplicationSys.enabled = wasEnabled + globalSiteReplicationSys.Unlock() + }() + + if err = globalSiteReplicationSys.healCORSMetadata(ctx, obj, bucket, info); err != nil { + t.Fatal(err) + } + got, err := globalBucketMetadataSys.Get(bucket) + if err != nil { + t.Fatal(err) + } + if !got.CorsConfigUpdatedAt.Equal(newer) { + t.Fatalf("heal retained source barrier %v, want %v", got.CorsConfigUpdatedAt, newer) + } +} + +func TestAdversarialBucketMetadataComparisonIsBase64CaseSensitive(t *testing.T) { + upper := "QQ==" + lower := "qQ==" + upperBytes, err := base64.StdEncoding.Strict().DecodeString(upper) + if err != nil { + t.Fatal(err) + } + lowerBytes, err := base64.StdEncoding.Strict().DecodeString(lower) + if err != nil { + t.Fatal(err) + } + if string(upperBytes) == string(lowerBytes) { + t.Fatal("test inputs unexpectedly decode to the same bytes") + } + if isBucketMetadataEqual(&upper, &lower) { + t.Fatal("different decoded payloads were treated as equal") + } +} + +func TestSiteReplicationStatusDetectsCorsTimestampMismatch(t *testing.T) { + ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{ + t: t, + objAPITest: testSiteReplicationStatusDetectsCorsTimestampMismatch, + endpoints: []string{"GetBucketCors"}, + }) +} + +func testSiteReplicationStatusDetectsCorsTimestampMismatch(obj ObjectLayer, _ string, bucket string, _ http.Handler, credentials auth.Credentials, t *testing.T) { + ctx := t.Context() + meta, err := readBucketMetadata(ctx, obj, bucket) + if err != nil { + t.Fatal(err) + } + older := meta.Created.Add(time.Second) + newer := older.Add(time.Second) + meta.CorsConfigXML = []byte(testSiteReplicationCORSDoc) + meta.CorsConfigUpdatedAt = older + if err = globalBucketMetadataSys.save(ctx, meta); err != nil { + t.Fatal(err) + } + + localID := globalDeploymentID() + remoteID := "remote-cors-status" + encoded := base64.StdEncoding.EncodeToString([]byte(testSiteReplicationCORSDoc)) + remoteInfo := madmin.SRInfo{ + DeploymentID: remoteID, + Buckets: map[string]madmin.SRBucketInfo{ + bucket: { + Bucket: bucket, + CreatedAt: meta.Created, + CorsConfig: &encoded, + CorsConfigUpdatedAt: newer, + }, + }, + } + remote := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(remoteInfo); err != nil { + t.Errorf("encode remote metadata: %v", err) + } + })) + defer remote.Close() + + serviceCred, err := auth.CreateCredentials("cors-status-service", "cors-status-service-secret-key") + if err != nil { + t.Fatal(err) + } + serviceCred.ParentUser = credentials.AccessKey + if _, err = globalIAMSys.store.AddServiceAccount(ctx, serviceCred); err != nil { + t.Fatal(err) + } + defer globalIAMSys.DeleteServiceAccount(ctx, serviceCred.AccessKey, false) + + globalSiteReplicationSys.Lock() + oldEnabled := globalSiteReplicationSys.enabled + oldState := globalSiteReplicationSys.state + globalSiteReplicationSys.enabled = true + globalSiteReplicationSys.state = srState{ + Name: "cors-status-test", + ServiceAccountAccessKey: serviceCred.AccessKey, + Peers: map[string]madmin.PeerInfo{ + localID: {Name: "local", DeploymentID: localID}, + remoteID: {Name: "remote", DeploymentID: remoteID, Endpoint: remote.URL}, + }, + } + globalSiteReplicationSys.Unlock() + defer func() { + globalSiteReplicationSys.Lock() + globalSiteReplicationSys.enabled = oldEnabled + globalSiteReplicationSys.state = oldState + globalSiteReplicationSys.Unlock() + }() + + status, err := globalSiteReplicationSys.siteReplicationStatus(ctx, obj, madmin.SRStatusOptions{Buckets: true}) + if err != nil { + t.Fatal(err) + } + localStatus, ok := status.BucketStats[bucket][localID] + if !ok { + t.Fatalf("status omitted local bucket entry: %#v", status.BucketStats[bucket]) + } + if !localStatus.CorsCfgMismatch { + t.Fatalf("status treated source timestamps %v and %v as converged", older, newer) + } +} + +func TestSiteReplicationStatusCountsCorsPerSite(t *testing.T) { + ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{ + t: t, + objAPITest: testSiteReplicationStatusCountsCorsPerSite, + endpoints: []string{"GetBucketCors"}, + }) +} + +func testSiteReplicationStatusCountsCorsPerSite(obj ObjectLayer, _ string, bucket string, _ http.Handler, credentials auth.Credentials, t *testing.T) { + ctx := t.Context() + meta, err := readBucketMetadata(ctx, obj, bucket) + if err != nil { + t.Fatal(err) + } + localID := globalDeploymentID() + remoteID := "remote-cors-count" + encoded := base64.StdEncoding.EncodeToString([]byte(testSiteReplicationCORSDoc)) + remoteInfo := madmin.SRInfo{ + DeploymentID: remoteID, + Buckets: map[string]madmin.SRBucketInfo{ + bucket: {Bucket: bucket, CreatedAt: meta.Created}, + }, + } + remote := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(remoteInfo); err != nil { + t.Errorf("encode remote metadata: %v", err) + } + })) + defer remote.Close() + + serviceCred, err := auth.CreateCredentials("cors-count-service", "cors-count-service-secret-key") + if err != nil { + t.Fatal(err) + } + serviceCred.ParentUser = credentials.AccessKey + if _, err = globalIAMSys.store.AddServiceAccount(ctx, serviceCred); err != nil { + t.Fatal(err) + } + defer globalIAMSys.DeleteServiceAccount(ctx, serviceCred.AccessKey, false) + + globalSiteReplicationSys.Lock() + oldEnabled := globalSiteReplicationSys.enabled + oldState := globalSiteReplicationSys.state + globalSiteReplicationSys.enabled = true + globalSiteReplicationSys.state = srState{ + Name: "cors-count-test", + ServiceAccountAccessKey: serviceCred.AccessKey, + Peers: map[string]madmin.PeerInfo{ + localID: {Name: "local", DeploymentID: localID}, + remoteID: {Name: "remote", DeploymentID: remoteID, Endpoint: remote.URL}, + }, + } + globalSiteReplicationSys.Unlock() + defer func() { + globalSiteReplicationSys.Lock() + globalSiteReplicationSys.enabled = oldEnabled + globalSiteReplicationSys.state = oldState + globalSiteReplicationSys.Unlock() + }() + + check := func(name string, wantLocal, wantRemote int, wantMismatch, wantReplicated bool) { + t.Helper() + status, err := globalSiteReplicationSys.siteReplicationStatus(ctx, obj, madmin.SRStatusOptions{Buckets: true}) + if err != nil { + t.Fatal(err) + } + if got := status.StatsSummary[localID].TotalCorsConfigCount; got != wantLocal { + t.Fatalf("%s: local TotalCorsConfigCount = %d, want %d", name, got, wantLocal) + } + if got := status.StatsSummary[remoteID].TotalCorsConfigCount; got != wantRemote { + t.Fatalf("%s: remote TotalCorsConfigCount = %d, want %d", name, got, wantRemote) + } + for _, id := range []string{localID, remoteID} { + bucketStatus := status.BucketStats[bucket][id] + wantSet := wantLocal != 0 + if id == remoteID { + wantSet = wantRemote != 0 + } + if bucketStatus.HasCorsCfgSet != wantSet { + t.Fatalf("%s: %s HasCorsCfgSet = %v, want %v", name, id, bucketStatus.HasCorsCfgSet, wantSet) + } + if bucketStatus.CorsCfgMismatch != wantMismatch { + t.Fatalf("%s: %s CorsCfgMismatch = %v, want %v", name, id, bucketStatus.CorsCfgMismatch, wantMismatch) + } + gotReplicated := status.StatsSummary[id].ReplicatedCorsConfig != 0 + if gotReplicated != wantReplicated { + t.Fatalf("%s: %s ReplicatedCorsConfig = %d, want replicated %v", name, id, status.StatsSummary[id].ReplicatedCorsConfig, wantReplicated) + } + } + } + + check("neither site", 0, 0, false, false) + t1 := meta.Created.Add(time.Second) + if err = globalSiteReplicationSys.PeerBucketCorsConfigHandler(ctx, bucket, &encoded, t1); err != nil { + t.Fatal(err) + } + check("local site only", 1, 0, true, false) + + t2 := t1.Add(time.Second) + if err = globalSiteReplicationSys.PeerBucketCorsConfigHandler(ctx, bucket, nil, t2); err != nil { + t.Fatal(err) + } + remoteInfo.Buckets[bucket] = madmin.SRBucketInfo{ + Bucket: bucket, CreatedAt: meta.Created, CorsConfig: &encoded, + } + check("live remote without timestamp", 0, 0, true, false) + + invalidXML := base64.StdEncoding.EncodeToString([]byte(`not xml`)) + remoteInfo.Buckets[bucket] = madmin.SRBucketInfo{ + Bucket: bucket, CreatedAt: meta.Created, CorsConfig: &invalidXML, CorsConfigUpdatedAt: t2, + } + check("invalid remote XML", 0, 0, true, false) + + remoteInfo.Buckets[bucket] = madmin.SRBucketInfo{ + Bucket: bucket, CreatedAt: meta.Created, CorsConfig: &encoded, CorsConfigUpdatedAt: t2, + } + check("remote site only", 0, 1, true, false) + + t3 := t2.Add(time.Second) + if err = globalSiteReplicationSys.PeerBucketCorsConfigHandler(ctx, bucket, &encoded, t3); err != nil { + t.Fatal(err) + } + remoteInfo.Buckets[bucket] = madmin.SRBucketInfo{ + Bucket: bucket, CreatedAt: meta.Created, CorsConfig: &encoded, CorsConfigUpdatedAt: t3, + } + check("both sites", 1, 1, false, true) +} + +func TestCORSReplicationStateOrdering(t *testing.T) { + at := UTCNow() + baseline := newCORSReplicationState(nil, time.Time{}) + liveA := newCORSReplicationState([]byte("a"), at) + liveB := newCORSReplicationState([]byte("b"), at) + tombstone := newCORSReplicationState(nil, at) + + ordered := []corsReplicationState{baseline, liveA, liveB, tombstone} + for i := 1; i < len(ordered); i++ { + if compareCORSReplicationStates(ordered[i-1], ordered[i]) >= 0 { + t.Fatalf("state %d is not lower than state %d", i-1, i) + } + } + for _, state := range ordered { + if !equalCORSReplicationStates(state, state) { + t.Fatalf("state is not equal to itself: %#v", state) + } + } +} + +func TestCORSReplicationStatusStateEquality(t *testing.T) { + at := UTCNow() + payload := base64.StdEncoding.EncodeToString([]byte(testSiteReplicationCORSDoc)) + otherPayload := base64.StdEncoding.EncodeToString([]byte(testSiteReplicationAlternateCORSDoc)) + sites := []srBucketMetaInfo{ + {DeploymentID: "a", SRBucketInfo: madmin.SRBucketInfo{}}, + {DeploymentID: "b", SRBucketInfo: madmin.SRBucketInfo{}}, + } + if !areCORSReplicationStatesEqual(sites) { + t.Fatal("two baselines should be converged") + } + + sites[0].CorsConfigUpdatedAt = at + sites[1].CorsConfigUpdatedAt = at + if !areCORSReplicationStatesEqual(sites) { + t.Fatal("matching tombstones should be converged") + } + sites[1].CorsConfigUpdatedAt = at.Add(time.Nanosecond) + if areCORSReplicationStatesEqual(sites) { + t.Fatal("different tombstone barriers should be mismatched") + } + + sites[0].CorsConfig = &payload + sites[1].CorsConfig = &payload + sites[1].CorsConfigUpdatedAt = at + if !areCORSReplicationStatesEqual(sites) { + t.Fatal("matching live states should be converged") + } + sites[1].CorsConfig = &otherPayload + if areCORSReplicationStatesEqual(sites) { + t.Fatal("different live payloads should be mismatched") + } +} + +func TestLatestCORSConfigEqualTimestampDeterministic(t *testing.T) { + at := UTCNow() + encodedA := base64.StdEncoding.EncodeToString([]byte(testSiteReplicationCORSDoc)) + encodedB := base64.StdEncoding.EncodeToString([]byte(testSiteReplicationAlternateCORSDoc)) + bs := map[string]srBucketStatsSummary{ + "site-a": {meta: srBucketMetaInfo{SRBucketInfo: madmin.SRBucketInfo{CorsConfig: &encodedA, CorsConfigUpdatedAt: at}}}, + "site-b": {meta: srBucketMetaInfo{SRBucketInfo: madmin.SRBucketInfo{CorsConfig: &encodedB, CorsConfigUpdatedAt: at}}}, + "site-c": {meta: srBucketMetaInfo{SRBucketInfo: madmin.SRBucketInfo{CorsConfigUpdatedAt: at}}}, + } + for i := 0; i < 100; i++ { + id, state, ok := latestCORSConfig(bs) + if !ok || id != "site-c" || state.kind != corsReplicationTombstone || !state.updatedAt.Equal(at) { + t.Fatalf("iteration %d selected (%q, %#v, %v), want site-c tombstone", i, id, state, ok) + } + } +} + +func TestNewBucketCORSReplicationEvent(t *testing.T) { + meta := newBucketMetadata("bucket") + if _, ok := newBucketCORSReplicationEvent(meta.Name, meta); ok { + t.Fatal("baseline unexpectedly produced an initial-sync event") + } + + at := UTCNow() + meta.CorsConfigXML = []byte(testSiteReplicationCORSDoc) + meta.CorsConfigUpdatedAt = at + live, ok := newBucketCORSReplicationEvent(meta.Name, meta) + if !ok || live.Cors == nil || !live.UpdatedAt.Equal(at) { + t.Fatalf("live initial-sync event = %#v, %v", live, ok) + } + decoded, err := base64.StdEncoding.Strict().DecodeString(*live.Cors) + if err != nil || string(decoded) != testSiteReplicationCORSDoc { + t.Fatalf("live initial-sync payload = %q, %v", decoded, err) + } + + meta.CorsConfigXML = nil + tombstone, ok := newBucketCORSReplicationEvent(meta.Name, meta) + if !ok || tombstone.Cors != nil || !tombstone.UpdatedAt.Equal(at) { + t.Fatalf("tombstone initial-sync event = %#v, %v", tombstone, ok) + } +} + +func TestPeerBucketCorsRejectsNonCanonicalBase64(t *testing.T) { + ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{ + t: t, + objAPITest: testPeerBucketCorsRejectsNonCanonicalBase64, + endpoints: []string{"GetBucketCors"}, + }) +} + +func testPeerBucketCorsRejectsNonCanonicalBase64(_ ObjectLayer, _ string, bucket string, _ http.Handler, _ auth.Credentials, t *testing.T) { + ctx := t.Context() + meta, err := globalBucketMetadataSys.Get(bucket) + if err != nil { + t.Fatal(err) + } + at := meta.Created.Add(time.Second) + inputs := []string{"AB==", "Q\nQ==", "!!!!"} + for _, encoded := range inputs { + if err = globalSiteReplicationSys.PeerBucketCorsConfigHandler(ctx, bucket, &encoded, at); err == nil { + t.Fatalf("non-canonical base64 %q was accepted", encoded) + } + if err = globalSiteReplicationSys.PeerBucketMetadataUpdateHandler(ctx, madmin.SRBucketMeta{ + Bucket: bucket, Cors: &encoded, UpdatedAt: at, + }); err == nil { + t.Fatalf("legacy bulk path accepted non-canonical base64 %q", encoded) + } + } + got, err := globalBucketMetadataSys.Get(bucket) + if err != nil { + t.Fatal(err) + } + if len(got.CorsConfigXML) != 0 || !got.CorsConfigUpdatedAt.IsZero() { + t.Fatalf("rejected payload changed metadata: xml=%q timestamp=%v", got.CorsConfigXML, got.CorsConfigUpdatedAt) + } +} + +func TestPeerBucketCorsRejectsInvalidConfiguration(t *testing.T) { + ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{ + t: t, + objAPITest: testPeerBucketCorsRejectsInvalidConfiguration, + endpoints: []string{"GetBucketCors"}, + }) +} + +func testPeerBucketCorsRejectsInvalidConfiguration(_ ObjectLayer, _ string, bucket string, _ http.Handler, _ auth.Credentials, t *testing.T) { + ctx := t.Context() + meta, err := globalBucketMetadataSys.Get(bucket) + if err != nil { + t.Fatal(err) + } + at := meta.Created.Add(time.Second) + invalidDocs := []string{ + `https://?.example.comGET`, + `not xml`, + } + for _, doc := range invalidDocs { + encoded := base64.StdEncoding.EncodeToString([]byte(doc)) + if err = globalSiteReplicationSys.PeerBucketCorsConfigHandler(ctx, bucket, &encoded, at); err == nil { + t.Fatalf("invalid CORS config %q was accepted", doc) + } + if err = globalSiteReplicationSys.PeerBucketMetadataUpdateHandler(ctx, madmin.SRBucketMeta{ + Bucket: bucket, Cors: &encoded, UpdatedAt: at, + }); err == nil { + t.Fatalf("legacy bulk path accepted invalid CORS config %q", doc) + } + } + got, err := globalBucketMetadataSys.Get(bucket) + if err != nil { + t.Fatal(err) + } + if len(got.CorsConfigXML) != 0 || !got.CorsConfigUpdatedAt.IsZero() { + t.Fatalf("rejected config changed metadata: xml=%q timestamp=%v", got.CorsConfigXML, got.CorsConfigUpdatedAt) + } +} + +func TestPeerBucketCorsCreatedAtFloor(t *testing.T) { + ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{ + t: t, + objAPITest: testPeerBucketCorsCreatedAtFloor, + endpoints: []string{"GetBucketCors"}, + }) +} + +func TestLegacyInvalidCorsMetadataCanBeDeleted(t *testing.T) { + ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{ + t: t, + objAPITest: testLegacyInvalidCorsMetadataCanBeDeleted, + endpoints: []string{"GetBucketCors"}, + }) +} + +func testLegacyInvalidCorsMetadataCanBeDeleted(obj ObjectLayer, _ string, bucket string, _ http.Handler, _ auth.Credentials, t *testing.T) { + ctx := t.Context() + meta, err := readBucketMetadata(ctx, obj, bucket) + if err != nil { + t.Fatal(err) + } + legacyAt := meta.Created.Add(time.Second) + meta.CorsConfigXML = []byte(`https://app.example.comget`) + meta.CorsConfigUpdatedAt = legacyAt + + data := make([]byte, 4, meta.Msgsize()+4) + binary.LittleEndian.PutUint16(data[0:2], bucketMetadataFormat) + binary.LittleEndian.PutUint16(data[2:4], bucketMetadataVersion) + data, err = meta.MarshalMsg(data) + if err != nil { + t.Fatal(err) + } + if err = saveConfig(ctx, obj, pathJoin(bucketMetaPrefix, bucket, bucketMetadataFile), data); err != nil { + t.Fatal(err) + } + + globalBucketMetadataSys.Remove(bucket) + loaded, err := globalBucketMetadataSys.GetConfigFromDisk(ctx, bucket) + if err != nil { + t.Fatalf("strict load made all bucket metadata unavailable: %v", err) + } + if loaded.corsConfigErr == nil || loaded.corsConfig != nil { + t.Fatalf("legacy CORS state = (%#v, %v), want fail-closed parse error", loaded.corsConfig, loaded.corsConfigErr) + } + globalBucketMetadataSys.Set(bucket, loaded) + 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) + } + + deleteAt, err := updateLocalBucketCORSMetadata(ctx, obj, bucket, nil) + if err != nil { + t.Fatalf("DELETE could not repair legacy invalid CORS: %v", err) + } + globalBucketMetadataSys.Remove(bucket) + repaired, err := globalBucketMetadataSys.GetConfigFromDisk(ctx, bucket) + if err != nil { + t.Fatal(err) + } + if repaired.corsConfigErr != nil || repaired.corsConfig != nil || len(repaired.CorsConfigXML) != 0 || !repaired.CorsConfigUpdatedAt.Equal(deleteAt) { + t.Fatalf("repaired CORS state = (%q, %#v, %v, %v), want tombstone at %v", repaired.CorsConfigXML, repaired.corsConfig, repaired.corsConfigErr, repaired.CorsConfigUpdatedAt, deleteAt) + } +} + +func testPeerBucketCorsCreatedAtFloor(_ ObjectLayer, _ string, bucket string, _ http.Handler, _ auth.Credentials, t *testing.T) { + ctx := t.Context() + meta, err := globalBucketMetadataSys.Get(bucket) + if err != nil { + t.Fatal(err) + } + encoded := base64.StdEncoding.EncodeToString([]byte(testSiteReplicationCORSDoc)) + if err = globalSiteReplicationSys.PeerBucketCorsConfigHandler(ctx, bucket, &encoded, meta.Created.Add(-time.Second)); err != nil { + t.Fatal(err) + } + got, err := globalBucketMetadataSys.Get(bucket) + if err != nil { + t.Fatal(err) + } + if len(got.CorsConfigXML) != 0 || !got.CorsConfigUpdatedAt.IsZero() { + t.Fatalf("pre-creation event changed metadata: xml=%q timestamp=%v", got.CorsConfigXML, got.CorsConfigUpdatedAt) + } + + fresh := meta.Created.Add(time.Second) + if err = globalSiteReplicationSys.PeerBucketCorsConfigHandler(ctx, bucket, &encoded, fresh); err != nil { + t.Fatal(err) + } + got, err = globalBucketMetadataSys.Get(bucket) + if err != nil { + t.Fatal(err) + } + if string(got.CorsConfigXML) != testSiteReplicationCORSDoc || !got.CorsConfigUpdatedAt.Equal(fresh) { + t.Fatalf("post-creation event state = (%q, %v), want live at %v", got.CorsConfigXML, got.CorsConfigUpdatedAt, fresh) + } +} + +func TestLocalBucketCorsUpdateAdvancesFutureBarrier(t *testing.T) { + ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{ + t: t, + objAPITest: testLocalBucketCorsUpdateAdvancesFutureBarrier, + endpoints: []string{"GetBucketCors"}, + }) +} + +func testLocalBucketCorsUpdateAdvancesFutureBarrier(obj ObjectLayer, _ string, bucket string, _ http.Handler, _ auth.Credentials, t *testing.T) { + ctx := t.Context() + meta, err := globalBucketMetadataSys.Get(bucket) + if err != nil { + t.Fatal(err) + } + future := UTCNow().Add(time.Hour) + if !future.After(meta.Created) { + future = meta.Created.Add(time.Hour) + } + if err = globalSiteReplicationSys.PeerBucketCorsConfigHandler(ctx, bucket, nil, future); err != nil { + t.Fatal(err) + } + updatedAt, err := updateLocalBucketCORSMetadata(ctx, obj, bucket, []byte(testSiteReplicationCORSDoc)) + if err != nil { + t.Fatal(err) + } + if !updatedAt.After(future) { + t.Fatalf("local update timestamp = %v, want after future barrier %v", updatedAt, future) + } + got, err := globalBucketMetadataSys.Get(bucket) + if err != nil { + t.Fatal(err) + } + if string(got.CorsConfigXML) != testSiteReplicationCORSDoc || !got.CorsConfigUpdatedAt.Equal(updatedAt) { + t.Fatalf("local update state = (%q, %v), want live payload at %v", got.CorsConfigXML, got.CorsConfigUpdatedAt, updatedAt) + } +} + +func TestLocalBucketCorsConcurrentUpdatesAreMonotonic(t *testing.T) { + ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{ + t: t, + objAPITest: testLocalBucketCorsConcurrentUpdatesAreMonotonic, + endpoints: []string{"GetBucketCors"}, + }) +} + +func testLocalBucketCorsConcurrentUpdatesAreMonotonic(obj ObjectLayer, _ string, bucket string, _ http.Handler, _ auth.Credentials, t *testing.T) { + type result struct { + payload []byte + at time.Time + err error + } + payloads := [][]byte{ + []byte(testSiteReplicationCORSDoc), + []byte(testSiteReplicationAlternateCORSDoc), + nil, + } + start := make(chan struct{}) + results := make(chan result, 24) + var wg sync.WaitGroup + for i := 0; i < cap(results); i++ { + payload := bytes.Clone(payloads[i%len(payloads)]) + wg.Add(1) + go func() { + defer wg.Done() + <-start + at, err := updateLocalBucketCORSMetadata(t.Context(), obj, bucket, payload) + results <- result{payload: payload, at: at, err: err} + }() + } + close(start) + wg.Wait() + close(results) + + seen := make(map[int64]struct{}, cap(results)) + var latest result + for got := range results { + if got.err != nil { + t.Fatal(got.err) + } + key := got.at.UnixNano() + if _, ok := seen[key]; ok { + t.Fatalf("concurrent local updates reused timestamp %v", got.at) + } + seen[key] = struct{}{} + if latest.at.IsZero() || got.at.After(latest.at) { + latest = got + } + } + + meta, err := globalBucketMetadataSys.Get(bucket) + if err != nil { + t.Fatal(err) + } + if !meta.CorsConfigUpdatedAt.Equal(latest.at) || !bytes.Equal(meta.CorsConfigXML, latest.payload) { + t.Fatalf("final state = (%q, %v), want last serialized local update (%q, %v)", meta.CorsConfigXML, meta.CorsConfigUpdatedAt, latest.payload, latest.at) + } +} + +func TestPeerBucketCorsConcurrentConvergence(t *testing.T) { + ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{ + t: t, + objAPITest: testPeerBucketCorsConcurrentConvergence, + endpoints: []string{"GetBucketCors"}, + }) +} + +func testPeerBucketCorsConcurrentConvergence(_ ObjectLayer, _ string, bucket string, _ http.Handler, _ auth.Credentials, t *testing.T) { + ctx := t.Context() + meta, err := globalBucketMetadataSys.Get(bucket) + if err != nil { + t.Fatal(err) + } + base := meta.Created.Add(time.Second) + encodedA := base64.StdEncoding.EncodeToString([]byte(testSiteReplicationCORSDoc)) + encodedB := base64.StdEncoding.EncodeToString([]byte(testSiteReplicationAlternateCORSDoc)) + events := []struct { + payload *string + at time.Time + }{ + {payload: &encodedA, at: base}, + {payload: &encodedB, at: base}, + {payload: nil, at: base}, + {payload: &encodedA, at: base.Add(time.Second)}, + {payload: &encodedB, at: base.Add(2 * time.Second)}, + {payload: nil, at: base.Add(2 * time.Second)}, + } + + start := make(chan struct{}) + errCh := make(chan error, len(events)*8) + var wg sync.WaitGroup + for i := 0; i < 8; i++ { + for j, event := range events { + event := event + useBulkPath := event.payload != nil && (i+j)%2 == 0 + wg.Add(1) + go func() { + defer wg.Done() + <-start + if useBulkPath { + errCh <- globalSiteReplicationSys.PeerBucketMetadataUpdateHandler(ctx, madmin.SRBucketMeta{ + Bucket: bucket, Cors: event.payload, UpdatedAt: event.at, + }) + return + } + errCh <- globalSiteReplicationSys.PeerBucketCorsConfigHandler(ctx, bucket, event.payload, event.at) + }() + } + } + close(start) + wg.Wait() + close(errCh) + for err := range errCh { + if err != nil { + t.Fatal(err) + } + } + + got, err := globalBucketMetadataSys.Get(bucket) + if err != nil { + t.Fatal(err) + } + if len(got.CorsConfigXML) != 0 || !got.CorsConfigUpdatedAt.Equal(base.Add(2*time.Second)) { + t.Fatalf("concurrent final state = (%q, %v), want newest equal-time tombstone", got.CorsConfigXML, got.CorsConfigUpdatedAt) + } +} + +func TestCorsReplicationDispatchStatusHealReload(t *testing.T) { + ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{ + t: t, + objAPITest: testCorsReplicationDispatchStatusHealReload, + endpoints: []string{"GetBucketCors"}, + }) +} + +func testCorsReplicationDispatchStatusHealReload(obj ObjectLayer, _ string, bucket string, _ http.Handler, credentials auth.Credentials, t *testing.T) { + ctx := t.Context() + meta, err := readBucketMetadata(ctx, obj, bucket) + if err != nil { + t.Fatal(err) + } + putAt := meta.Created.Add(time.Second) + deleteAt := putAt.Add(time.Second) + encoded := base64.StdEncoding.EncodeToString([]byte(testSiteReplicationCORSDoc)) + item, err := json.Marshal(madmin.SRBucketMeta{ + Type: madmin.SRBucketMetaTypeCorsConfig, + Bucket: bucket, + Cors: &encoded, + UpdatedAt: putAt, + }) + if err != nil { + t.Fatal(err) + } + + adminRouter := mux.NewRouter() + registerAdminRouter(adminRouter, true) + path := adminPathPrefix + adminAPIVersionPrefix + "/site-replication/peer/bucket-meta" + req, err := newTestSignedRequestV4(http.MethodPut, path, int64(len(item)), bytes.NewReader(item), credentials.AccessKey, credentials.SecretKey, nil) + if err != nil { + t.Fatal(err) + } + rec := httptest.NewRecorder() + adminRouter.ServeHTTP(rec, req) + if rec.Code != http.StatusOK { + t.Fatalf("admin dispatch returned %d: %s", rec.Code, rec.Body.String()) + } + + localID := globalDeploymentID() + remoteID := "remote-cors-integration" + remoteInfo := madmin.SRInfo{ + DeploymentID: remoteID, + Buckets: map[string]madmin.SRBucketInfo{ + bucket: { + Bucket: bucket, + CreatedAt: meta.Created, + CorsConfig: nil, + CorsConfigUpdatedAt: deleteAt, + }, + }, + } + remoteApplies := make(chan madmin.SRBucketMeta, 1) + remote := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method == http.MethodPut { + var applied madmin.SRBucketMeta + if err := json.NewDecoder(r.Body).Decode(&applied); err != nil { + t.Errorf("decode remote apply: %v", err) + w.WriteHeader(http.StatusBadRequest) + return + } + remoteApplies <- applied + w.WriteHeader(http.StatusOK) + return + } + w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(remoteInfo); err != nil { + t.Errorf("encode remote metadata: %v", err) + } + })) + defer remote.Close() + + serviceCred, err := auth.CreateCredentials("cors-integration-svc", "cors-integration-service-secret") + if err != nil { + t.Fatal(err) + } + serviceCred.ParentUser = credentials.AccessKey + if _, err = globalIAMSys.store.AddServiceAccount(ctx, serviceCred); err != nil { + t.Fatal(err) + } + defer globalIAMSys.DeleteServiceAccount(ctx, serviceCred.AccessKey, false) + + globalSiteReplicationSys.Lock() + oldEnabled := globalSiteReplicationSys.enabled + oldState := globalSiteReplicationSys.state + globalSiteReplicationSys.enabled = true + globalSiteReplicationSys.state = srState{ + Name: "cors-integration-test", + ServiceAccountAccessKey: serviceCred.AccessKey, + Peers: map[string]madmin.PeerInfo{ + localID: {Name: "local", DeploymentID: localID}, + remoteID: {Name: "remote", DeploymentID: remoteID, Endpoint: remote.URL}, + }, + } + globalSiteReplicationSys.Unlock() + defer func() { + globalSiteReplicationSys.Lock() + globalSiteReplicationSys.enabled = oldEnabled + globalSiteReplicationSys.state = oldState + globalSiteReplicationSys.Unlock() + }() + + status, err := globalSiteReplicationSys.siteReplicationStatus(ctx, obj, madmin.SRStatusOptions{Buckets: true}) + if err != nil { + t.Fatal(err) + } + if !status.BucketStats[bucket][localID].CorsCfgMismatch { + t.Fatal("status did not expose the missed DELETE") + } + if err = globalSiteReplicationSys.healCORSMetadata(ctx, obj, bucket, status); err != nil { + t.Fatal(err) + } + + globalBucketMetadataSys.Remove(bucket) + reloaded, err := globalBucketMetadataSys.GetConfigFromDisk(ctx, bucket) + if err != nil { + t.Fatal(err) + } + globalBucketMetadataSys.Set(bucket, reloaded) + if len(reloaded.CorsConfigXML) != 0 || reloaded.corsConfig != nil || !reloaded.CorsConfigUpdatedAt.Equal(deleteAt) { + t.Fatalf("reloaded state = (%q, %#v, %v), want tombstone at %v", reloaded.CorsConfigXML, reloaded.corsConfig, reloaded.CorsConfigUpdatedAt, deleteAt) + } + metaInfo, err := globalSiteReplicationSys.SiteReplicationMetaInfo(ctx, obj, madmin.SRStatusOptions{Buckets: true}) + if err != nil { + t.Fatal(err) + } + got := metaInfo.Buckets[bucket] + if got.CorsConfig != nil || !got.CorsConfigUpdatedAt.Equal(deleteAt) { + t.Fatalf("post-reload status = (%v, %v), want tombstone at %v", got.CorsConfig, got.CorsConfigUpdatedAt, deleteAt) + } + + // Make the local site the winner and exercise heal's remote dispatch branch. + newerAt := deleteAt.Add(time.Second) + if err = globalSiteReplicationSys.PeerBucketCorsConfigHandler(ctx, bucket, &encoded, newerAt); err != nil { + t.Fatal(err) + } + status, err = globalSiteReplicationSys.siteReplicationStatus(ctx, obj, madmin.SRStatusOptions{Buckets: true}) + if err != nil { + t.Fatal(err) + } + if err = globalSiteReplicationSys.healCORSMetadata(ctx, obj, bucket, status); err != nil { + t.Fatal(err) + } + select { + case applied := <-remoteApplies: + if applied.Type != madmin.SRBucketMetaTypeCorsConfig || applied.Bucket != bucket || applied.Cors == nil || !applied.UpdatedAt.Equal(newerAt) { + t.Fatalf("remote heal apply = %#v, want live CORS at %v", applied, newerAt) + } + decoded, err := base64.StdEncoding.Strict().DecodeString(*applied.Cors) + if err != nil || string(decoded) != testSiteReplicationCORSDoc { + t.Fatalf("remote heal payload = %q, %v", decoded, err) + } + case <-time.After(5 * time.Second): + t.Fatal("remote heal did not dispatch CORS state") + } +} diff --git a/cmd/bucket-encryption-handlers.go b/cmd/bucket-encryption-handlers.go index 1fe7631de..5b1efa834 100644 --- a/cmd/bucket-encryption-handlers.go +++ b/cmd/bucket-encryption-handlers.go @@ -30,7 +30,7 @@ import ( "github.com/minio/minio/internal/kms" "github.com/minio/minio/internal/logger" "github.com/minio/mux" - "github.com/minio/pkg/v3/policy" + "github.com/pgsty/silo-pkg/v3/policy" ) const ( diff --git a/cmd/bucket-handlers.go b/cmd/bucket-handlers.go index 9be23c21d..93e14d837 100644 --- a/cmd/bucket-handlers.go +++ b/cmd/bucket-handlers.go @@ -62,8 +62,8 @@ import ( "github.com/minio/minio/internal/ioutil" "github.com/minio/minio/internal/kms" "github.com/minio/minio/internal/logger" - "github.com/minio/pkg/v3/policy" - "github.com/minio/pkg/v3/sync/errgroup" + "github.com/pgsty/silo-pkg/v3/policy" + "github.com/pgsty/silo-pkg/v3/sync/errgroup" ) const ( @@ -463,13 +463,20 @@ func (api objectAPIHandlers) DeleteMultipleObjectsHandler(w http.ResponseWriter, // Make sure to update context to print ObjectNames for multi objects. ctx = updateReqContext(ctx, objects...) - // Call checkRequestAuthType to populate ReqInfo.AccessKey before GetBucketInfo() - // Ignore errors here to preserve the S3 error behavior of GetBucketInfo() - checkRequestAuthType(ctx, r, policy.DeleteObjectAction, bucket, "") - deleteObjectsFn := objectAPI.DeleteObjects - // Return Malformed XML as S3 spec if the number of objects is empty + reqInfo := logger.GetReqInfo(ctx) + if reqInfo == nil { + writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrAccessDenied), r.URL) + return + } + reqInfo.BucketName = bucket + reqInfo.ObjectName = "" + if s3Err := authenticateRequest(ctx, r, policy.DeleteObjectAction); s3Err != ErrNone { + writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Err), r.URL) + return + } + // Return Malformed XML as S3 spec if the number of objects is empty. if len(deleteObjectsReq.Objects) == 0 || len(deleteObjectsReq.Objects) > maxDeleteList { writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrMalformedXML), r.URL) return @@ -499,11 +506,9 @@ func (api objectAPIHandlers) DeleteMultipleObjectsHandler(w http.ResponseWriter, vc, _ := globalBucketVersioningSys.Get(bucket) oss := make([]*objSweeper, len(deleteObjectsReq.Objects)) for index, object := range deleteObjectsReq.Objects { - if apiErrCode := checkRequestAuthTypeWithVID(ctx, r, policy.DeleteObjectAction, bucket, object.ObjectName, object.VersionID); apiErrCode != ErrNone { - if apiErrCode == ErrSignatureDoesNotMatch || apiErrCode == ErrInvalidAccessKeyID { - writeErrorResponse(ctx, w, errorCodes.ToAPIErr(apiErrCode), r.URL) - return - } + reqInfo.ObjectName = object.ObjectName + reqInfo.VersionID = object.VersionID + if apiErrCode := authorizeRequest(ctx, r, deleteObjectAction(object.VersionID)); apiErrCode != ErrNone { apiErr := errorCodes.ToAPIErr(apiErrCode) deleteResults[index].errInfo = DeleteError{ Code: apiErr.Code, @@ -1844,12 +1849,7 @@ func (api objectAPIHandlers) PutBucketObjectLockConfigHandler(w http.ResponseWri // We encode the xml bytes as base64 to ensure there are no encoding // errors. cfgStr := base64.StdEncoding.EncodeToString(configData) - replLogIf(ctx, globalSiteReplicationSys.BucketMetaHook(ctx, madmin.SRBucketMeta{ - Type: madmin.SRBucketMetaTypeObjectLockConfig, - Bucket: bucket, - ObjectLockConfig: &cfgStr, - UpdatedAt: updatedAt, - })) + replLogIf(ctx, globalSiteReplicationSys.BucketMetaHook(ctx, newSRBucketObjectLockMeta(bucket, &cfgStr, updatedAt))) // Write success response. writeSuccessResponseHeadersOnly(w) diff --git a/cmd/bucket-handlers_test.go b/cmd/bucket-handlers_test.go index c8972508f..32c041dff 100644 --- a/cmd/bucket-handlers_test.go +++ b/cmd/bucket-handlers_test.go @@ -24,12 +24,52 @@ import ( "io" "net/http" "net/http/httptest" + "net/url" "strconv" "testing" "github.com/minio/minio/internal/auth" ) +func TestListObjectsNonExistentBucketHandler(t *testing.T) { + ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{t: t, objAPITest: testListObjectsNonExistentBucketHandler}) +} + +func testListObjectsNonExistentBucketHandler(_ ObjectLayer, instanceType, _ string, apiRouter http.Handler, + credentials auth.Credentials, t *testing.T, +) { + const bucket = "missing-bucket" + testCases := []struct { + name string + query url.Values + }{ + {name: "ListObjects", query: url.Values{"prefix": {"/"}}}, + {name: "ListObjectsV2", query: url.Values{"list-type": {"2"}, "prefix": {"/"}}}, + {name: "ListObjectVersions", query: url.Values{"versions": {""}, "prefix": {"/"}}}, + } + + for _, tc := range testCases { + req, err := newTestSignedRequestV4(http.MethodGet, makeTestTargetURL("", bucket, "", tc.query), 0, nil, + credentials.AccessKey, credentials.SecretKey, nil) + if err != nil { + t.Fatalf("%s: %s: failed to create request: %v", instanceType, tc.name, err) + } + rec := httptest.NewRecorder() + apiRouter.ServeHTTP(rec, req) + if rec.Code != http.StatusNotFound { + t.Errorf("%s: %s: expected status %d, got %d", instanceType, tc.name, http.StatusNotFound, rec.Code) + } + + var apiErr APIErrorResponse + if err = xml.Unmarshal(rec.Body.Bytes(), &apiErr); err != nil { + t.Fatalf("%s: %s: failed to decode error response: %v", instanceType, tc.name, err) + } + if apiErr.Code != "NoSuchBucket" { + t.Errorf("%s: %s: expected NoSuchBucket, got %q", instanceType, tc.name, apiErr.Code) + } + } +} + // Wrapper for calling RemoveBucket HTTP handler tests for both Erasure multiple disks and single node setup. func TestRemoveBucketHandler(t *testing.T) { ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{t: t, objAPITest: testRemoveBucketHandler, endpoints: []string{"RemoveBucket"}}) @@ -978,14 +1018,23 @@ func testAPIDeleteMultipleObjectsVersionIDNullCondition(obj ObjectLayer, instanc policyBytes := fmt.Appendf(nil, `{ "Version":"2012-10-17", - "Statement":[{ - "Effect":"Allow", - "Principal":"*", - "Action":"s3:DeleteObject", - "Resource":"arn:aws:s3:::%s/*", - "Condition":{"Null":{"s3:versionid":"true"}} - }] - }`, bucketName) + "Statement":[ + { + "Effect":"Allow", + "Principal":"*", + "Action":"s3:DeleteObject", + "Resource":"arn:aws:s3:::%s/*", + "Condition":{"Null":{"s3:versionid":"true"}} + }, + { + "Effect":"Allow", + "Principal":"*", + "Action":"s3:DeleteObjectVersion", + "Resource":"arn:aws:s3:::%s/*", + "Condition":{"StringEquals":{"s3:versionid":"%s"}} + } + ] + }`, bucketName, bucketName, versionIDs["with-version-id"]) policyReq, err := newTestSignedRequestV4(http.MethodPut, getPutPolicyURL("", bucketName), int64(len(policyBytes)), bytes.NewReader(policyBytes), credentials.AccessKey, credentials.SecretKey, nil) if err != nil { @@ -1031,29 +1080,30 @@ func testAPIDeleteMultipleObjectsVersionIDNullCondition(obj ObjectLayer, instanc t.Errorf("%s: %q was not a successful delete-marker creation: %+v", instanceType, objectName, response.DeletedObjects) } } - if len(deleted) != 2 { + if object, ok := deleted["with-version-id"]; !ok || object.VersionID != versionIDs["with-version-id"] { + t.Errorf("%s: matching explicit version was not deleted: %+v", instanceType, response.DeletedObjects) + } + if len(deleted) != 3 { t.Errorf("%s: unexpected deleted objects: %+v", instanceType, response.DeletedObjects) } errorsByKey := make(map[string]DeleteError, len(response.Errors)) for _, deleteErr := range response.Errors { errorsByKey[deleteErr.Key] = deleteErr } - for objectName, versionID := range map[string]string{ - "with-version-id": versionIDs["with-version-id"], - "with-null-version-id": nullVersionID, - } { + for objectName, versionID := range map[string]string{"with-null-version-id": nullVersionID} { deleteErr, ok := errorsByKey[objectName] if !ok || deleteErr.VersionID != versionID || deleteErr.Code != errorCodes[ErrAccessDenied].Code { t.Errorf("%s: %q did not return AccessDenied for version %q: %+v", instanceType, objectName, versionID, response.Errors) } } - if len(errorsByKey) != 2 { + if len(errorsByKey) != 1 { t.Errorf("%s: unexpected delete errors: %+v", instanceType, response.Errors) } - // A simple delete adds a marker and keeps the old version. The explicitly - // named version must also remain because its policy condition did not match. - for objectName, versionID := range versionIDs { + // A simple delete adds a marker and keeps the old version. The null-version + // delete remains denied because its per-entry condition does not match. + for _, objectName := range []string{"without-version-id-before", "without-version-id-after", "with-null-version-id"} { + versionID := versionIDs[objectName] if _, err = obj.GetObjectInfo(t.Context(), bucketName, objectName, ObjectOptions{VersionID: versionID}); err != nil { t.Errorf("%s: version %s of %q was not preserved: %v", instanceType, versionID, objectName, err) } @@ -1063,7 +1113,10 @@ func testAPIDeleteMultipleObjectsVersionIDNullCondition(obj ObjectLayer, instanc t.Errorf("%s: simple delete of %q did not hide the latest object behind a delete marker: %v", instanceType, objectName, err) } } - for _, objectName := range []string{"with-version-id", "with-null-version-id"} { + if _, err = obj.GetObjectInfo(t.Context(), bucketName, "with-version-id", ObjectOptions{VersionID: versionIDs["with-version-id"]}); !isErrVersionNotFound(err) && !isErrObjectNotFound(err) { + t.Errorf("%s: matching explicit version still exists: %v", instanceType, err) + } + for _, objectName := range []string{"with-null-version-id"} { if info, err := obj.GetObjectInfo(t.Context(), bucketName, objectName, ObjectOptions{}); err != nil { t.Errorf("%s: denied version delete removed latest %q: %v", instanceType, objectName, err) } else if info.VersionID != versionIDs[objectName] { diff --git a/cmd/bucket-lifecycle-handlers.go b/cmd/bucket-lifecycle-handlers.go index e917c9adf..971124023 100644 --- a/cmd/bucket-lifecycle-handlers.go +++ b/cmd/bucket-lifecycle-handlers.go @@ -27,7 +27,7 @@ import ( xhttp "github.com/minio/minio/internal/http" "github.com/minio/minio/internal/logger" "github.com/minio/mux" - "github.com/minio/pkg/v3/policy" + "github.com/pgsty/silo-pkg/v3/policy" ) const ( diff --git a/cmd/bucket-lifecycle.go b/cmd/bucket-lifecycle.go index 24fdc67d1..866343bc1 100644 --- a/cmd/bucket-lifecycle.go +++ b/cmd/bucket-lifecycle.go @@ -41,7 +41,7 @@ import ( xhttp "github.com/minio/minio/internal/http" "github.com/minio/minio/internal/logger" "github.com/minio/minio/internal/s3select" - xnet "github.com/minio/pkg/v3/net" + xnet "github.com/pgsty/silo-pkg/v3/net" "github.com/zeebo/xxh3" ) diff --git a/cmd/bucket-listobjects-handlers.go b/cmd/bucket-listobjects-handlers.go index 0afd76c43..75dd448c0 100644 --- a/cmd/bucket-listobjects-handlers.go +++ b/cmd/bucket-listobjects-handlers.go @@ -26,7 +26,7 @@ import ( "github.com/minio/minio/internal/logger" "github.com/minio/mux" - "github.com/minio/pkg/v3/policy" + "github.com/pgsty/silo-pkg/v3/policy" ) // Validate all the ListObjects query arguments, returns an APIErrorCode diff --git a/cmd/bucket-metadata-lock_test.go b/cmd/bucket-metadata-lock_test.go new file mode 100644 index 000000000..4e2a4af83 --- /dev/null +++ b/cmd/bucket-metadata-lock_test.go @@ -0,0 +1,455 @@ +// Copyright (c) 2015-2026 MinIO, Inc. +// Copyright (c) 2026 PGSTY +// +// This file is part of MinIO Object Storage stack +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +package cmd + +import ( + "bytes" + "context" + "fmt" + "net/http" + "sync" + "sync/atomic" + "testing" + "time" + + "github.com/minio/madmin-go/v3" + "github.com/minio/minio/internal/auth" + "github.com/minio/minio/internal/bucket/lifecycle" + "github.com/minio/minio/internal/bucket/versioning" +) + +type metadataRMWWriterKey struct{} + +type metadataRMWBarrierObjectLayer struct { + ObjectLayer + bucket string + aReady chan struct{} + aRelease chan struct{} + bLockAttempt chan struct{} + aReadyOnce sync.Once + bLockOnce sync.Once + cancelOnce sync.Once + cancelOnPut context.CancelFunc + reads atomic.Int64 +} + +func (o *metadataRMWBarrierObjectLayer) metadataObject() string { + return pathJoin(bucketMetaPrefix, o.bucket, bucketMetadataFile) +} + +func (o *metadataRMWBarrierObjectLayer) metadataLock() string { + return pathJoin(bucketMetaPrefix, o.bucket, "metadata.lock") +} + +func (o *metadataRMWBarrierObjectLayer) GetObjectNInfo(ctx context.Context, bucket, object string, rs *HTTPRangeSpec, h http.Header, opts ObjectOptions) (*GetObjectReader, error) { + if bucket == minioMetaBucket && object == o.metadataObject() { + o.reads.Add(1) + } + return o.ObjectLayer.GetObjectNInfo(ctx, bucket, object, rs, h, opts) +} + +func (o *metadataRMWBarrierObjectLayer) PutObject(ctx context.Context, bucket, object string, data *PutObjReader, opts ObjectOptions) (ObjectInfo, error) { + if bucket == minioMetaBucket && object == o.metadataObject() && o.cancelOnPut != nil { + o.cancelOnce.Do(o.cancelOnPut) + } + if bucket == minioMetaBucket && object == o.metadataObject() && ctx.Value(metadataRMWWriterKey{}) == "A" { + o.aReadyOnce.Do(func() { close(o.aReady) }) + select { + case <-o.aRelease: + case <-ctx.Done(): + return ObjectInfo{}, ctx.Err() + } + } + return o.ObjectLayer.PutObject(ctx, bucket, object, data, opts) +} + +func (o *metadataRMWBarrierObjectLayer) NewNSLock(bucket string, objects ...string) RWLocker { + lock := o.ObjectLayer.NewNSLock(bucket, objects...) + if bucket != minioMetaBucket || len(objects) != 1 || objects[0] != o.metadataLock() { + return lock + } + return metadataObservedRWLocker{RWLocker: lock, onLock: func(ctx context.Context) { + if ctx.Value(metadataRMWWriterKey{}) == "B" { + o.bLockOnce.Do(func() { close(o.bLockAttempt) }) + } + }} +} + +type metadataObservedRWLocker struct { + RWLocker + onLock func(context.Context) +} + +func (l metadataObservedRWLocker) GetLock(ctx context.Context, timeout *dynamicTimeout) (LockContext, error) { + l.onLock(ctx) + return l.RWLocker.GetLock(ctx, timeout) +} + +func TestBucketMetadataLockPreservesPolicyAndCORS(t *testing.T) { + defer DetectTestLeak(t)() + ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{ + t: t, + objAPITest: testBucketMetadataLockPreservesPolicyAndCORS, + }) +} + +func testBucketMetadataLockPreservesPolicyAndCORS(obj ObjectLayer, instanceType, bucket string, + _ http.Handler, _ auth.Credentials, t *testing.T, +) { + policyJSON := fmt.Appendf(nil, `{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":"*","Action":"s3:GetObject","Resource":"arn:aws:s3:::%s/*"}]}`, bucket) + corsXML := []byte(testSiteReplicationCORSDoc) + runBucketMetadataRMWConflict(t, obj, bucket, + func(ctx context.Context, objectAPI ObjectLayer) error { + _, err := globalBucketMetadataSys.Update(ctx, bucket, bucketPolicyConfig, policyJSON) + return err + }, + func(ctx context.Context, objectAPI ObjectLayer) error { + _, err := updateLocalBucketCORSMetadata(ctx, objectAPI, bucket, corsXML) + return err + }, + func(meta BucketMetadata) bool { + return bytes.Equal(meta.PolicyConfigJSON, policyJSON) && bytes.Equal(meta.CorsConfigXML, corsXML) + }, instanceType+": policy+CORS") +} + +func TestBucketMetadataLockPreservesTaggingAndSSE(t *testing.T) { + defer DetectTestLeak(t)() + ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{ + t: t, + objAPITest: testBucketMetadataLockPreservesTaggingAndSSE, + }) +} + +func TestBucketMetadataLockPreservesPeerBulkAndLocalUpdate(t *testing.T) { + defer DetectTestLeak(t)() + ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{ + t: t, + objAPITest: testBucketMetadataLockPreservesPeerBulkAndLocalUpdate, + }) +} + +func testBucketMetadataLockPreservesPeerBulkAndLocalUpdate(obj ObjectLayer, instanceType, bucket string, + _ http.Handler, _ auth.Credentials, t *testing.T, +) { + meta, err := readBucketMetadata(t.Context(), obj, bucket) + if err != nil { + t.Fatal(err) + } + policyJSON := fmt.Appendf(nil, `{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":"*","Action":"s3:GetObject","Resource":"arn:aws:s3:::%s/*"}]}`, bucket) + tagXML := []byte(`localtag`) + runBucketMetadataRMWConflict(t, obj, bucket, + func(ctx context.Context, objectAPI ObjectLayer) error { + return globalSiteReplicationSys.PeerBucketMetadataUpdateHandler(ctx, madmin.SRBucketMeta{ + Bucket: bucket, Policy: policyJSON, UpdatedAt: meta.Created.Add(time.Second), + }) + }, + func(ctx context.Context, objectAPI ObjectLayer) error { + _, err := globalBucketMetadataSys.Update(ctx, bucket, bucketTaggingConfig, tagXML) + return err + }, + func(meta BucketMetadata) bool { + return bytes.Equal(meta.PolicyConfigJSON, policyJSON) && bytes.Equal(meta.TaggingConfigXML, tagXML) + }, instanceType+": peer bulk+local tagging") +} + +func TestBucketMetadataLockPreservesLifecycleDeleteAndSSE(t *testing.T) { + defer DetectTestLeak(t)() + ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{ + t: t, + objAPITest: testBucketMetadataLockPreservesLifecycleDeleteAndSSE, + }) +} + +func testBucketMetadataLockPreservesLifecycleDeleteAndSSE(obj ObjectLayer, instanceType, bucket string, + _ http.Handler, _ auth.Credentials, t *testing.T, +) { + lifecycleXML := []byte(`expirelogs/Enabled30`) + if _, err := globalBucketMetadataSys.Update(t.Context(), bucket, bucketLifecycleConfig, lifecycleXML); err != nil { + t.Fatal(err) + } + sseXML := []byte(`AES256`) + runBucketMetadataRMWConflict(t, obj, bucket, + func(ctx context.Context, objectAPI ObjectLayer) error { + _, err := globalBucketMetadataSys.Delete(ctx, bucket, bucketLifecycleConfig) + return err + }, + func(ctx context.Context, objectAPI ObjectLayer) error { + _, err := globalBucketMetadataSys.Update(ctx, bucket, bucketSSEConfig, sseXML) + return err + }, + func(meta BucketMetadata) bool { + cfg, err := lifecycle.ParseLifecycleConfig(bytes.NewReader(meta.LifecycleConfigXML)) + return err == nil && cfg.ExpiryUpdatedAt != nil && len(cfg.Rules) == 0 && bytes.Equal(meta.EncryptionConfigXML, sseXML) + }, instanceType+": lifecycle delete+SSE") +} + +func TestMakeBucketForceCreatePreservesMetadata(t *testing.T) { + defer DetectTestLeak(t)() + ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{ + t: t, + objAPITest: testMakeBucketForceCreatePreservesMetadata, + }) +} + +func testMakeBucketForceCreatePreservesMetadata(obj ObjectLayer, instanceType, bucket string, + _ http.Handler, _ auth.Credentials, t *testing.T, +) { + ctx := t.Context() + policyJSON := fmt.Appendf(nil, `{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":"*","Action":"s3:GetObject","Resource":"arn:aws:s3:::%s/*"}]}`, bucket) + corsXML := []byte(testSiteReplicationCORSDoc) + if _, err := globalBucketMetadataSys.Update(ctx, bucket, bucketPolicyConfig, policyJSON); err != nil { + t.Fatal(err) + } + if _, err := updateLocalBucketCORSMetadata(ctx, obj, bucket, corsXML); err != nil { + t.Fatal(err) + } + before, err := readBucketMetadata(ctx, obj, bucket) + if err != nil { + t.Fatal(err) + } + if err = obj.MakeBucket(ctx, bucket, MakeBucketOptions{ForceCreate: true}); err != nil { + t.Fatalf("%s: ForceCreate existing bucket: %v", instanceType, err) + } + after, err := readBucketMetadata(ctx, obj, bucket) + if err != nil { + t.Fatal(err) + } + if !after.Created.Equal(before.Created) || !bytes.Equal(after.PolicyConfigJSON, policyJSON) || !bytes.Equal(after.CorsConfigXML, corsXML) { + t.Fatalf("%s: ForceCreate replaced metadata: before=%+v after=%+v", instanceType, before, after) + } +} + +func TestApplyImportedBucketMetadataPreservesUnspecifiedFields(t *testing.T) { + policyJSON := []byte(`{"Version":"2012-10-17","Statement":[]}`) + tagXML := []byte(`existingtag`) + src := newBucketMetadata("bucket") + src.PolicyConfigJSON = policyJSON + src.PolicyConfigUpdatedAt = UTCNow() + dst := newBucketMetadata("bucket") + dst.TaggingConfigXML = bytes.Clone(tagXML) + + applyImportedBucketMetadata(&dst, src, importMetadataFields{bucketPolicyConfig: {}}) + if !bytes.Equal(dst.PolicyConfigJSON, policyJSON) || !bytes.Equal(dst.TaggingConfigXML, tagXML) { + t.Fatalf("import patch overwrote unspecified metadata: %+v", dst) + } + src.PolicyConfigJSON[0] = '!' + if dst.PolicyConfigJSON[0] == '!' { + t.Fatal("import patch retained the source byte slice") + } +} + +func TestMakeBucketDoesNotAdoptGhostMetadata(t *testing.T) { + defer DetectTestLeak(t)() + ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{ + t: t, + objAPITest: testMakeBucketDoesNotAdoptGhostMetadata, + }) +} + +func testMakeBucketDoesNotAdoptGhostMetadata(obj ObjectLayer, instanceType, _ string, + _ http.Handler, _ auth.Credentials, t *testing.T, +) { + ctx := t.Context() + bucket := getRandomBucketName() + if err := obj.MakeBucket(ctx, bucket, MakeBucketOptions{}); err != nil { + t.Fatal(err) + } + policyJSON := fmt.Appendf(nil, `{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":"*","Action":"s3:GetObject","Resource":"arn:aws:s3:::%s/*"}]}`, bucket) + if _, err := globalBucketMetadataSys.Update(ctx, bucket, bucketPolicyConfig, policyJSON); err != nil { + t.Fatal(err) + } + oldMeta, err := readBucketMetadata(ctx, obj, bucket) + if err != nil { + t.Fatal(err) + } + z, ok := obj.(*erasureServerPools) + if !ok { + t.Fatalf("%s: object layer is %T, want *erasureServerPools", instanceType, obj) + } + if err = z.s3Peer.DeleteBucket(ctx, bucket, DeleteBucketOptions{Force: true}); err != nil { + t.Fatalf("%s: delete bucket volume only: %v", instanceType, err) + } + if err = obj.MakeBucket(ctx, bucket, MakeBucketOptions{}); err != nil { + t.Fatalf("%s: recreate bucket: %v", instanceType, err) + } + newMeta, err := readBucketMetadata(ctx, obj, bucket) + if err != nil { + t.Fatal(err) + } + if bytes.Equal(newMeta.PolicyConfigJSON, policyJSON) || newMeta.Created.Equal(oldMeta.Created) { + t.Fatalf("%s: new bucket adopted ghost metadata: old=%+v new=%+v", instanceType, oldMeta, newMeta) + } +} + +func TestMakeBucketForceCreateLockEnablesVersioning(t *testing.T) { + defer DetectTestLeak(t)() + ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{ + t: t, + objAPITest: testMakeBucketForceCreateLockEnablesVersioning, + }) +} + +func TestPeerBucketMetadataSaveSurvivesCallerCancellation(t *testing.T) { + defer DetectTestLeak(t)() + ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{ + t: t, + objAPITest: testPeerBucketMetadataSaveSurvivesCallerCancellation, + }) +} + +func testPeerBucketMetadataSaveSurvivesCallerCancellation(obj ObjectLayer, instanceType, bucket string, + _ http.Handler, _ auth.Credentials, t *testing.T, +) { + previousObjectAPI := newObjectLayerFn() + ctx, cancel := context.WithCancel(t.Context()) + barrier := &metadataRMWBarrierObjectLayer{ + ObjectLayer: obj, + bucket: bucket, + cancelOnPut: cancel, + } + setObjectLayer(barrier) + defer setObjectLayer(previousObjectAPI) + + err := globalSiteReplicationSys.PeerBucketMakeWithVersioningHandler(ctx, bucket, MakeBucketOptions{VersioningEnabled: true}) + if err != nil { + t.Fatalf("%s: peer metadata save failed after caller cancellation: %v", instanceType, err) + } + if ctx.Err() != context.Canceled { + t.Fatalf("%s: metadata write did not trigger caller cancellation", instanceType) + } + meta, err := readBucketMetadata(t.Context(), obj, bucket) + if err != nil { + t.Fatal(err) + } + cfg, err := versioning.ParseConfig(bytes.NewReader(meta.VersioningConfigXML)) + if err != nil { + t.Fatal(err) + } + if !cfg.Enabled() { + t.Fatalf("%s: peer metadata save lost versioning after cancellation", instanceType) + } +} + +func testMakeBucketForceCreateLockEnablesVersioning(obj ObjectLayer, instanceType, bucket string, + _ http.Handler, _ auth.Credentials, t *testing.T, +) { + ctx := t.Context() + suspended := []byte(`Suspended`) + if _, err := globalBucketMetadataSys.Update(ctx, bucket, bucketVersioningConfig, suspended); err != nil { + t.Fatal(err) + } + if err := obj.MakeBucket(ctx, bucket, MakeBucketOptions{ForceCreate: true, LockEnabled: true}); err != nil { + t.Fatalf("%s: ForceCreate with object lock: %v", instanceType, err) + } + meta, err := readBucketMetadata(ctx, obj, bucket) + if err != nil { + t.Fatal(err) + } + cfg, err := versioning.ParseConfig(bytes.NewReader(meta.VersioningConfigXML)) + if err != nil { + t.Fatal(err) + } + if !cfg.Enabled() || len(meta.ObjectLockConfigXML) == 0 { + t.Fatalf("%s: object lock state lacks enabled versioning: metadata=%+v", instanceType, meta) + } +} + +func testBucketMetadataLockPreservesTaggingAndSSE(obj ObjectLayer, instanceType, bucket string, + _ http.Handler, _ auth.Credentials, t *testing.T, +) { + tagXML := []byte(`keyvalue`) + sseXML := []byte(`AES256`) + runBucketMetadataRMWConflict(t, obj, bucket, + func(ctx context.Context, objectAPI ObjectLayer) error { + _, err := globalBucketMetadataSys.Update(ctx, bucket, bucketTaggingConfig, tagXML) + return err + }, + func(ctx context.Context, objectAPI ObjectLayer) error { + _, err := globalBucketMetadataSys.Update(ctx, bucket, bucketSSEConfig, sseXML) + return err + }, + func(meta BucketMetadata) bool { + return bytes.Equal(meta.TaggingConfigXML, tagXML) && bytes.Equal(meta.EncryptionConfigXML, sseXML) + }, instanceType+": tagging+SSE") +} + +func runBucketMetadataRMWConflict(t *testing.T, obj ObjectLayer, bucket string, + writerA, writerB func(context.Context, ObjectLayer) error, + complete func(BucketMetadata) bool, name string, +) { + t.Helper() + previousObjectAPI := newObjectLayerFn() + barrier := &metadataRMWBarrierObjectLayer{ + ObjectLayer: obj, + bucket: bucket, + aReady: make(chan struct{}), + aRelease: make(chan struct{}), + bLockAttempt: make(chan struct{}), + } + setObjectLayer(barrier) + defer setObjectLayer(previousObjectAPI) + + ctx, cancel := context.WithTimeout(t.Context(), 10*time.Second) + defer cancel() + aCtx := context.WithValue(ctx, metadataRMWWriterKey{}, "A") + bCtx := context.WithValue(ctx, metadataRMWWriterKey{}, "B") + aDone := make(chan error, 1) + bDone := make(chan error, 1) + go func() { aDone <- writerA(aCtx, barrier) }() + + select { + case <-barrier.aReady: + case <-ctx.Done(): + t.Fatalf("%s: writer A did not reach metadata save: %v", name, ctx.Err()) + } + go func() { bDone <- writerB(bCtx, barrier) }() + + var ( + bErr error + bFinished bool + ) + select { + case <-barrier.bLockAttempt: + if got := barrier.reads.Load(); got != 1 { + t.Fatalf("%s: writer B read metadata before acquiring metadata.lock: reads=%d", name, got) + } + case bErr = <-bDone: + bFinished = true + case <-ctx.Done(): + t.Fatalf("%s: writer B neither completed nor attempted metadata.lock: %v", name, ctx.Err()) + } + close(barrier.aRelease) + if err := <-aDone; err != nil { + t.Fatalf("%s: writer A failed: %v", name, err) + } + if !bFinished { + select { + case bErr = <-bDone: + case <-ctx.Done(): + t.Fatalf("%s: writer B did not finish: %v", name, ctx.Err()) + } + } + if bErr != nil { + t.Fatalf("%s: writer B failed: %v", name, bErr) + } + + disk, err := readBucketMetadata(ctx, barrier, bucket) + if err != nil { + t.Fatalf("%s: read disk metadata: %v", name, err) + } + resident, err := globalBucketMetadataSys.Get(bucket) + if err != nil { + t.Fatalf("%s: read resident metadata: %v", name, err) + } + if !complete(disk) || !complete(resident) { + t.Fatalf("%s: concurrent updates lost a field: disk=%+v resident=%+v", name, disk, resident) + } +} diff --git a/cmd/bucket-metadata-sys.go b/cmd/bucket-metadata-sys.go index 20be4ffd3..6d510d52e 100644 --- a/cmd/bucket-metadata-sys.go +++ b/cmd/bucket-metadata-sys.go @@ -29,6 +29,7 @@ import ( "github.com/minio/madmin-go/v3" "github.com/minio/minio-go/v7/pkg/set" "github.com/minio/minio-go/v7/pkg/tags" + "github.com/minio/minio/internal/bucket/cors" bucketsse "github.com/minio/minio/internal/bucket/encryption" "github.com/minio/minio/internal/bucket/lifecycle" objectlock "github.com/minio/minio/internal/bucket/object/lock" @@ -37,8 +38,8 @@ import ( "github.com/minio/minio/internal/event" "github.com/minio/minio/internal/kms" "github.com/minio/minio/internal/logger" - "github.com/minio/pkg/v3/policy" - "github.com/minio/pkg/v3/sync/errgroup" + "github.com/pgsty/silo-pkg/v3/policy" + "github.com/pgsty/silo-pkg/v3/sync/errgroup" "golang.org/x/sync/singleflight" ) @@ -50,8 +51,27 @@ type BucketMetadataSys struct { initialized bool group *singleflight.Group metadataMap map[string]BucketMetadata + // loadFailed records real buckets whose metadata has never been loaded + // successfully because the startup load or a refresh failed. 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. It never holds a resident bucket, is + // bounded by the number of failed loads, and is empty in normal operation. + loadFailed map[string]struct{} } +// noteLoadFailure and clearLoadFailure maintain loadFailed; both expect the +// caller to hold sys.Lock. A bucket that is resident keeps its last loaded +// metadata through a failed refresh, exactly like every other bucket +// configuration, so the set only ever holds non-resident buckets. +func (sys *BucketMetadataSys) noteLoadFailure(bucket string) { + if _, resident := sys.metadataMap[bucket]; !resident { + 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() @@ -66,6 +86,7 @@ func (sys *BucketMetadataSys) Remove(buckets ...string) { for _, bucket := range buckets { sys.group.Forget(bucket) delete(sys.metadataMap, bucket) + sys.clearLoadFailure(bucket) globalBucketMonitor.DeleteBucket(bucket) } sys.Unlock() @@ -83,6 +104,11 @@ func (sys *BucketMetadataSys) RemoveStaleBuckets(diskBuckets set.StringSet) { delete(sys.metadataMap, bucket) globalBucketMonitor.DeleteBucket(bucket) } + for bucket := range sys.loadFailed { + if !diskBuckets.Contains(bucket) { + sys.clearLoadFailure(bucket) + } + } } // Set - sets a new metadata in-memory. @@ -94,11 +120,12 @@ func (sys *BucketMetadataSys) Set(bucket string, meta BucketMetadata) { if !isMinioMetaBucketName(bucket) { sys.Lock() sys.metadataMap[bucket] = meta + sys.clearLoadFailure(bucket) sys.Unlock() } } -func (sys *BucketMetadataSys) updateAndParse(ctx context.Context, bucket string, configFile string, configData []byte, parse bool) (updatedAt time.Time, err error) { +func (sys *BucketMetadataSys) updateAndParse(ctx context.Context, bucket string, configFile string, configData []byte, parse, lifecycleDelete bool) (updatedAt time.Time, err error) { objAPI := newObjectLayerFn() if objAPI == nil { return updatedAt, errServerNotInitialized @@ -107,60 +134,78 @@ func (sys *BucketMetadataSys) updateAndParse(ctx context.Context, bucket string, if isMinioMetaBucketName(bucket) { return updatedAt, errInvalidArgument } - - meta, err := loadBucketMetadataParse(ctx, objAPI, bucket, parse) + notifyCtx := ctx + ctx, unlock, err := lockBucketMetadata(ctx, objAPI, bucket) if err != nil { - if !globalIsErasure && !globalIsDistErasure && errors.Is(err, errVolumeNotFound) { - // Only single drive mode needs this fallback. - meta = newBucketMetadata(bucket) - } else { - return updatedAt, err - } - } - updatedAt = UTCNow() - switch configFile { - case bucketPolicyConfig: - meta.PolicyConfigJSON = configData - meta.PolicyConfigUpdatedAt = updatedAt - case bucketNotificationConfig: - meta.NotificationConfigXML = configData - meta.NotificationConfigUpdatedAt = updatedAt - case bucketLifecycleConfig: - meta.LifecycleConfigXML = configData - meta.LifecycleConfigUpdatedAt = updatedAt - case bucketSSEConfig: - meta.EncryptionConfigXML = configData - meta.EncryptionConfigUpdatedAt = updatedAt - case bucketTaggingConfig: - meta.TaggingConfigXML = configData - meta.TaggingConfigUpdatedAt = updatedAt - case bucketQuotaConfigFile: - meta.QuotaConfigJSON = configData - meta.QuotaConfigUpdatedAt = updatedAt - case objectLockConfig: - meta.ObjectLockConfigXML = configData - meta.ObjectLockConfigUpdatedAt = updatedAt - case bucketVersioningConfig: - meta.VersioningConfigXML = configData - meta.VersioningConfigUpdatedAt = updatedAt - case bucketReplicationConfig: - meta.ReplicationConfigXML = configData - meta.ReplicationConfigUpdatedAt = updatedAt - case bucketTargetsFile: - meta.BucketTargetsConfigJSON, meta.BucketTargetsConfigMetaJSON, err = encryptBucketMetadata(ctx, meta.Name, configData, kms.Context{ - bucket: meta.Name, - bucketTargetsFile: bucketTargetsFile, - }) - if err != nil { - return updatedAt, fmt.Errorf("Error encrypting bucket target metadata %w", err) - } - meta.BucketTargetsConfigUpdatedAt = updatedAt - meta.BucketTargetsConfigMetaUpdatedAt = updatedAt - default: - return updatedAt, fmt.Errorf("Unknown bucket %s metadata update requested %s", bucket, configFile) + return updatedAt, err } - return updatedAt, sys.save(ctx, meta) + err = func() error { + defer unlock() + meta, err := loadBucketMetadataParse(ctx, objAPI, bucket, parse) + if err != nil { + if !globalIsErasure && !globalIsDistErasure && errors.Is(err, errVolumeNotFound) { + // Only single drive mode needs this fallback. + meta = newBucketMetadata(bucket) + } else { + return err + } + } + if lifecycleDelete { + configData, err = lifecycleDeleteConfig(meta.LifecycleConfigXML) + if err != nil { + return err + } + } + updatedAt = UTCNow() + switch configFile { + case bucketPolicyConfig: + meta.PolicyConfigJSON = configData + meta.PolicyConfigUpdatedAt = updatedAt + case bucketNotificationConfig: + meta.NotificationConfigXML = configData + meta.NotificationConfigUpdatedAt = updatedAt + case bucketLifecycleConfig: + meta.LifecycleConfigXML = configData + meta.LifecycleConfigUpdatedAt = updatedAt + case bucketSSEConfig: + meta.EncryptionConfigXML = configData + meta.EncryptionConfigUpdatedAt = updatedAt + case bucketTaggingConfig: + meta.TaggingConfigXML = configData + meta.TaggingConfigUpdatedAt = updatedAt + case bucketQuotaConfigFile: + meta.QuotaConfigJSON = configData + meta.QuotaConfigUpdatedAt = updatedAt + case objectLockConfig: + meta.ObjectLockConfigXML = configData + meta.ObjectLockConfigUpdatedAt = updatedAt + case bucketVersioningConfig: + meta.VersioningConfigXML = configData + meta.VersioningConfigUpdatedAt = updatedAt + case bucketReplicationConfig: + meta.ReplicationConfigXML = configData + meta.ReplicationConfigUpdatedAt = updatedAt + case bucketTargetsFile: + meta.BucketTargetsConfigJSON, meta.BucketTargetsConfigMetaJSON, err = encryptBucketMetadata(ctx, meta.Name, configData, kms.Context{ + bucket: meta.Name, + bucketTargetsFile: bucketTargetsFile, + }) + if err != nil { + return fmt.Errorf("Error encrypting bucket target metadata %w", err) + } + meta.BucketTargetsConfigUpdatedAt = updatedAt + meta.BucketTargetsConfigMetaUpdatedAt = updatedAt + default: + return fmt.Errorf("Unknown bucket %s metadata update requested %s", bucket, configFile) + } + return sys.saveMetadata(ctx, objAPI, meta) + }() + if err != nil { + return updatedAt, err + } + globalNotificationSys.LoadBucketMetadata(bgContext(notifyCtx), bucket) // Do not use caller context here + return updatedAt, nil } func (sys *BucketMetadataSys) save(ctx context.Context, meta BucketMetadata) error { @@ -173,59 +218,78 @@ func (sys *BucketMetadataSys) save(ctx context.Context, meta BucketMetadata) err return errInvalidArgument } - if err := meta.Save(ctx, objAPI); err != nil { + if err := sys.saveMetadata(ctx, objAPI, meta); err != nil { return err } - sys.Set(meta.Name, meta) globalNotificationSys.LoadBucketMetadata(bgContext(ctx), meta.Name) // Do not use caller context here return nil } +// saveMetadata persists and publishes metadata locally. Callers performing a +// read-modify-write must hold metadata.lock and release it before peer fan-out. +func (sys *BucketMetadataSys) saveMetadata(ctx context.Context, objAPI ObjectLayer, meta BucketMetadata) error { + if err := meta.Save(ctx, objAPI); err != nil { + return err + } + sys.Set(meta.Name, meta) + return nil +} + +func lockBucketMetadata(ctx context.Context, objectAPI ObjectLayer, bucket string) (context.Context, func(), error) { + return lockBucketMetadataWithTimeout(ctx, objectAPI, bucket, globalOperationTimeout) +} + +func lockBucketMetadataWithTimeout(ctx context.Context, objectAPI ObjectLayer, bucket string, timeout *dynamicTimeout) (context.Context, func(), error) { + lock := objectAPI.NewNSLock(minioMetaBucket, pathJoin(bucketMetaPrefix, bucket, "metadata.lock")) + lkctx, err := lock.GetLock(ctx, timeout) + if err != nil { + return nil, nil, err + } + ctx = context.WithValue(lkctx.Context(), bucketMetadataLockContextKey{}, bucket) + return ctx, func() { lock.Unlock(lkctx) }, nil +} + +type bucketMetadataLockContextKey struct{} + +func bucketMetadataLockHeld(ctx context.Context, bucket string) bool { + lockedBucket, _ := ctx.Value(bucketMetadataLockContextKey{}).(string) + return lockedBucket == bucket +} + // Delete delete the bucket metadata for the specified bucket. // must be used by all callers instead of using Update() with nil configData. func (sys *BucketMetadataSys) Delete(ctx context.Context, bucket string, configFile string) (updatedAt time.Time, err error) { - if configFile == bucketLifecycleConfig { - // Get bucket config from current site - meta, e := globalBucketMetadataSys.GetConfigFromDisk(ctx, bucket) - if e != nil && !errors.Is(e, errConfigNotFound) { - return updatedAt, e - } - var expiryRuleRemoved bool - if len(meta.LifecycleConfigXML) > 0 { - var lcCfg lifecycle.Lifecycle - if err := xml.Unmarshal(meta.LifecycleConfigXML, &lcCfg); err != nil { - return updatedAt, err - } - // find a single expiry rule set the flag - for _, rl := range lcCfg.Rules { - if !rl.Expiration.IsNull() || !rl.NoncurrentVersionExpiration.IsNull() { - expiryRuleRemoved = true - break - } - } - } + return sys.updateAndParse(ctx, bucket, configFile, nil, false, configFile == bucketLifecycleConfig) +} - // Form empty ILM details with `ExpiryUpdatedAt` field and save - var cfgData []byte - if expiryRuleRemoved { - var lcCfg lifecycle.Lifecycle - currtime := time.Now() - lcCfg.ExpiryUpdatedAt = &currtime - cfgData, err = xml.Marshal(lcCfg) - if err != nil { - return updatedAt, err +func lifecycleDeleteConfig(current []byte) ([]byte, error) { + var expiryRuleRemoved bool + if len(current) > 0 { + var lcCfg lifecycle.Lifecycle + if err := xml.Unmarshal(current, &lcCfg); err != nil { + return nil, err + } + for _, rl := range lcCfg.Rules { + if !rl.Expiration.IsNull() || !rl.NoncurrentVersionExpiration.IsNull() { + expiryRuleRemoved = true + break } } - return sys.updateAndParse(ctx, bucket, configFile, cfgData, false) } - return sys.updateAndParse(ctx, bucket, configFile, nil, false) + if !expiryRuleRemoved { + return nil, nil + } + var lcCfg lifecycle.Lifecycle + currtime := time.Now() + lcCfg.ExpiryUpdatedAt = &currtime + return xml.Marshal(lcCfg) } // Update update bucket metadata for the specified bucket. // The configData data should not be modified after being sent here. func (sys *BucketMetadataSys) Update(ctx context.Context, bucket string, configFile string, configData []byte) (updatedAt time.Time, err error) { - return sys.updateAndParse(ctx, bucket, configFile, configData, true) + return sys.updateAndParse(ctx, bucket, configFile, configData, true, false) } // Get metadata for a bucket. @@ -359,6 +423,57 @@ func (sys *BucketMetadataSys) GetSSEConfig(bucket string) (*bucketsse.BucketSSEC return meta.sseConfig, meta.EncryptionConfigUpdatedAt, nil } +// 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 isReservedOrInvalidBucket(bucket, true) { + return nil, time.Time{}, errConfigNotFound + } + sys.RLock() + meta, ok := sys.metadataMap[bucket] + _, failed := sys.loadFailed[bucket] + initialized := sys.initialized + sys.RUnlock() + if !ok { + if !initialized || failed { + return nil, time.Time{}, errBucketMetadataNotInitialized + } + return nil, time.Time{}, errConfigNotFound + } + 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 +} + +// GetCorsConfigXML returns the raw stored CORS configuration XML for the +// given bucket, preserving the document exactly as it was PUT (including +// the S3 xmlns and any unmodeled elements). +func (sys *BucketMetadataSys) GetCorsConfigXML(bucket string) ([]byte, 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 len(meta.CorsConfigXML) == 0 { + return nil, time.Time{}, errConfigNotFound + } + return meta.CorsConfigXML, meta.CorsConfigUpdatedAt, nil +} + // CreatedAt returns the time of creation of bucket func (sys *BucketMetadataSys) CreatedAt(bucket string) (time.Time, error) { meta, _, err := sys.GetConfig(GlobalContext, bucket) @@ -488,6 +603,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 @@ -539,8 +655,10 @@ func (sys *BucketMetadataSys) concurrentLoad(ctx context.Context, buckets []stri sys.Lock() for i, meta := range bucketMetas { if errs[i] != nil { + sys.noteLoadFailure(buckets[i]) continue } + sys.clearLoadFailure(buckets[i]) sys.metadataMap[buckets[i]] = meta } sys.Unlock() @@ -590,6 +708,9 @@ func (sys *BucketMetadataSys) refreshBucketsMetadataLoop(ctx context.Context) { meta, err := loadBucketMetadata(ctx, sys.objAPI, bucket) if err != nil { internalLogIf(ctx, err, logger.WarningKind) + sys.Lock() + sys.noteLoadFailure(bucket) + sys.Unlock() wait() // wait to proceed to next entry. continue } @@ -600,6 +721,7 @@ func (sys *BucketMetadataSys) refreshBucketsMetadataLoop(ctx context.Context) { updated = true sys.metadataMap[bucket] = meta } + sys.clearLoadFailure(bucket) sys.Unlock() if updated { @@ -647,6 +769,7 @@ func (sys *BucketMetadataSys) init(ctx context.Context, buckets []string) { func (sys *BucketMetadataSys) Reset() { sys.Lock() clear(sys.metadataMap) + clear(sys.loadFailed) sys.Unlock() } @@ -654,6 +777,7 @@ func (sys *BucketMetadataSys) Reset() { func NewBucketMetadataSys() *BucketMetadataSys { return &BucketMetadataSys{ metadataMap: make(map[string]BucketMetadata), + loadFailed: make(map[string]struct{}), group: &singleflight.Group{}, } } diff --git a/cmd/bucket-metadata.go b/cmd/bucket-metadata.go index e78118175..c93ce9907 100644 --- a/cmd/bucket-metadata.go +++ b/cmd/bucket-metadata.go @@ -31,6 +31,7 @@ import ( "github.com/minio/madmin-go/v3" "github.com/minio/minio-go/v7/pkg/tags" + "github.com/minio/minio/internal/bucket/cors" bucketsse "github.com/minio/minio/internal/bucket/encryption" "github.com/minio/minio/internal/bucket/lifecycle" objectlock "github.com/minio/minio/internal/bucket/object/lock" @@ -40,8 +41,8 @@ import ( "github.com/minio/minio/internal/event" "github.com/minio/minio/internal/kms" "github.com/minio/minio/internal/logger" - "github.com/minio/pkg/v3/policy" "github.com/minio/sio" + "github.com/pgsty/silo-pkg/v3/policy" ) const ( @@ -58,6 +59,9 @@ var ( enabledBucketVersioningConfig = []byte(`Enabled`) ) +// Bucket CORS configuration file. +const bucketCorsConfig = "cors.xml" + //go:generate msgp -file $GOFILE // BucketMetadata contains bucket metadata. @@ -80,6 +84,7 @@ type BucketMetadata struct { ReplicationConfigXML []byte BucketTargetsConfigJSON []byte BucketTargetsConfigMetaJSON []byte + CorsConfigXML []byte PolicyConfigUpdatedAt time.Time ObjectLockConfigUpdatedAt time.Time @@ -92,6 +97,7 @@ type BucketMetadata struct { NotificationConfigUpdatedAt time.Time BucketTargetsConfigUpdatedAt time.Time BucketTargetsConfigMetaUpdatedAt time.Time + CorsConfigUpdatedAt time.Time // Add a new UpdatedAt field and update lastUpdate function // Unexported fields. Must be updated atomically. @@ -106,6 +112,8 @@ type BucketMetadata struct { replicationConfig *replication.Config bucketTargetConfig *madmin.BucketTargets bucketTargetConfigMeta map[string]string + corsConfig *cors.Config + corsConfigErr error } // newBucketMetadata creates BucketMetadata with the supplied name and Created to Now. @@ -160,6 +168,9 @@ func (b BucketMetadata) lastUpdate() (t time.Time) { if b.BucketTargetsConfigMetaUpdatedAt.After(t) { t = b.BucketTargetsConfigMetaUpdatedAt } + if b.CorsConfigUpdatedAt.After(t) { + t = b.CorsConfigUpdatedAt + } return t } @@ -238,8 +249,17 @@ func loadBucketMetadataParse(ctx context.Context, objectAPI ObjectLayer, bucket } if len(configs) > 0 { - // Old bucket without bucket metadata. Hence we migrate existing settings. - if err = b.convertLegacyConfigs(ctx, objectAPI, configs); err != nil { + if !bucketMetadataLockHeld(ctx, bucket) { + migrated, lockErr := loadBucketMetadataParseUnderLock(ctx, objectAPI, bucket, parse) + if lockErr == nil { + return migrated, nil + } + if !errors.Is(lockErr, errBucketMetadataMigrationLockUnavailable) { + return b, lockErr + } + internalLogOnceIf(ctx, fmt.Errorf("unable to persist bucket metadata migration for %s, using the legacy configuration in memory: %w", bucket, lockErr), "bucket-metadata-migration-lock-"+bucket) + b.applyLegacyConfigs(configs) + } else if err = b.convertLegacyConfigs(ctx, objectAPI, configs); err != nil { return b, err } } @@ -251,8 +271,25 @@ func loadBucketMetadataParse(ctx context.Context, objectAPI ObjectLayer, bucket return b, err } } + if b.corsConfigErr != nil { + // Keep the rest of the bucket metadata available so an operator can + // replace or delete a CORS document accepted by an older, more lenient + // build. Defer unrelated metadata migration until CORS is repaired. + return b, nil + } // migrate unencrypted remote targets + if len(b.BucketTargetsConfigJSON) != 0 && GlobalKMS != nil && len(b.BucketTargetsConfigMetaJSON) == 0 && !bucketMetadataLockHeld(ctx, bucket) { + migrated, lockErr := loadBucketMetadataParseUnderLock(ctx, objectAPI, bucket, parse) + if lockErr == nil { + return migrated, nil + } + if !errors.Is(lockErr, errBucketMetadataMigrationLockUnavailable) { + return b, lockErr + } + internalLogOnceIf(ctx, fmt.Errorf("unable to persist encrypted bucket target metadata for %s, using the existing configuration in memory: %w", bucket, lockErr), "bucket-metadata-migration-lock-"+bucket) + return b, nil + } if err = b.migrateTargetConfig(ctx, objectAPI); err != nil { return b, err } @@ -260,6 +297,20 @@ func loadBucketMetadataParse(ctx context.Context, objectAPI ObjectLayer, bucket return b, nil } +func loadBucketMetadataParseUnderLock(ctx context.Context, objectAPI ObjectLayer, bucket string, parse bool) (BucketMetadata, error) { + ctx, unlock, err := lockBucketMetadataWithTimeout(ctx, objectAPI, bucket, bucketMetadataMigrationTimeout) + if err != nil { + return newBucketMetadata(bucket), fmt.Errorf("%w: %v", errBucketMetadataMigrationLockUnavailable, err) + } + defer unlock() + return loadBucketMetadataParse(ctx, objectAPI, bucket, parse) +} + +var ( + bucketMetadataMigrationTimeout = newDynamicTimeout(5*time.Second, time.Second) + errBucketMetadataMigrationLockUnavailable = errors.New("bucket metadata migration lock unavailable") +) + // loadBucketMetadata loads and migrates to bucket metadata. func loadBucketMetadata(ctx context.Context, objectAPI ObjectLayer, bucket string) (BucketMetadata, error) { return loadBucketMetadataParse(ctx, objectAPI, bucket, true) @@ -310,8 +361,20 @@ func (b *BucketMetadata) parseAllConfigs(ctx context.Context, objectAPI ObjectLa b.taggingConfig = nil } - if bytes.Equal(b.ObjectLockConfigXML, enabledBucketObjectLockConfig) { - b.VersioningConfigXML = enabledBucketVersioningConfig + b.corsConfigErr = nil + if len(b.CorsConfigXML) != 0 { + cfg, corsErr := cors.ParseBucketCorsConfig(bytes.NewReader(b.CorsConfigXML)) + if corsErr == nil { + corsErr = cfg.Validate() + } + if corsErr != nil { + b.corsConfig = nil + b.corsConfigErr = fmt.Errorf("invalid bucket CORS configuration: %w", corsErr) + } else { + b.corsConfig = cfg + } + } else { + b.corsConfig = nil } if len(b.ObjectLockConfigXML) != 0 { @@ -322,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)) @@ -404,7 +476,7 @@ func (b *BucketMetadata) getAllLegacyConfigs(ctx context.Context, objectAPI Obje return configs, nil } -func (b *BucketMetadata) convertLegacyConfigs(ctx context.Context, objectAPI ObjectLayer, configs map[string][]byte) error { +func (b *BucketMetadata) applyLegacyConfigs(configs map[string][]byte) { for legacyFile, configData := range configs { switch legacyFile { case legacyBucketObjectLockEnabledConfigFile: @@ -436,6 +508,10 @@ func (b *BucketMetadata) convertLegacyConfigs(ctx context.Context, objectAPI Obj } } b.defaultTimestamps() +} + +func (b *BucketMetadata) convertLegacyConfigs(ctx context.Context, objectAPI ObjectLayer, configs map[string][]byte) error { + b.applyLegacyConfigs(configs) if err := b.Save(ctx, objectAPI); err != nil { return err @@ -503,6 +579,9 @@ func (b *BucketMetadata) Save(ctx context.Context, api ObjectLayer) error { if err := b.parseAllConfigs(ctx, api); err != nil { return err } + if b.corsConfigErr != nil { + return b.corsConfigErr + } data := make([]byte, 4, b.Msgsize()+4) diff --git a/cmd/bucket-metadata_gen.go b/cmd/bucket-metadata_gen.go index 0407b66ea..b074b3e13 100644 --- a/cmd/bucket-metadata_gen.go +++ b/cmd/bucket-metadata_gen.go @@ -108,6 +108,12 @@ func (z *BucketMetadata) DecodeMsg(dc *msgp.Reader) (err error) { err = msgp.WrapError(err, "BucketTargetsConfigMetaJSON") return } + case "CorsConfigXML": + z.CorsConfigXML, err = dc.ReadBytes(z.CorsConfigXML) + if err != nil { + err = msgp.WrapError(err, "CorsConfigXML") + return + } case "PolicyConfigUpdatedAt": z.PolicyConfigUpdatedAt, err = dc.ReadTime() if err != nil { @@ -174,6 +180,12 @@ func (z *BucketMetadata) DecodeMsg(dc *msgp.Reader) (err error) { err = msgp.WrapError(err, "BucketTargetsConfigMetaUpdatedAt") return } + case "CorsConfigUpdatedAt": + z.CorsConfigUpdatedAt, err = dc.ReadTime() + if err != nil { + err = msgp.WrapError(err, "CorsConfigUpdatedAt") + return + } default: err = dc.Skip() if err != nil { @@ -187,9 +199,9 @@ func (z *BucketMetadata) DecodeMsg(dc *msgp.Reader) (err error) { // EncodeMsg implements msgp.Encodable func (z *BucketMetadata) EncodeMsg(en *msgp.Writer) (err error) { - // map header, size 25 + // map header, size 27 // write "Name" - err = en.Append(0xde, 0x0, 0x19, 0xa4, 0x4e, 0x61, 0x6d, 0x65) + err = en.Append(0xde, 0x0, 0x1b, 0xa4, 0x4e, 0x61, 0x6d, 0x65) if err != nil { return } @@ -328,6 +340,16 @@ func (z *BucketMetadata) EncodeMsg(en *msgp.Writer) (err error) { err = msgp.WrapError(err, "BucketTargetsConfigMetaJSON") return } + // write "CorsConfigXML" + err = en.Append(0xad, 0x43, 0x6f, 0x72, 0x73, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x58, 0x4d, 0x4c) + if err != nil { + return + } + err = en.WriteBytes(z.CorsConfigXML) + if err != nil { + err = msgp.WrapError(err, "CorsConfigXML") + return + } // write "PolicyConfigUpdatedAt" err = en.Append(0xb5, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74) if err != nil { @@ -438,15 +460,25 @@ func (z *BucketMetadata) EncodeMsg(en *msgp.Writer) (err error) { err = msgp.WrapError(err, "BucketTargetsConfigMetaUpdatedAt") return } + // write "CorsConfigUpdatedAt" + err = en.Append(0xb3, 0x43, 0x6f, 0x72, 0x73, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74) + if err != nil { + return + } + err = en.WriteTime(z.CorsConfigUpdatedAt) + if err != nil { + err = msgp.WrapError(err, "CorsConfigUpdatedAt") + return + } return } // MarshalMsg implements msgp.Marshaler func (z *BucketMetadata) MarshalMsg(b []byte) (o []byte, err error) { o = msgp.Require(b, z.Msgsize()) - // map header, size 25 + // map header, size 27 // string "Name" - o = append(o, 0xde, 0x0, 0x19, 0xa4, 0x4e, 0x61, 0x6d, 0x65) + o = append(o, 0xde, 0x0, 0x1b, 0xa4, 0x4e, 0x61, 0x6d, 0x65) o = msgp.AppendString(o, z.Name) // string "Created" o = append(o, 0xa7, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64) @@ -487,6 +519,9 @@ func (z *BucketMetadata) MarshalMsg(b []byte) (o []byte, err error) { // string "BucketTargetsConfigMetaJSON" o = append(o, 0xbb, 0x42, 0x75, 0x63, 0x6b, 0x65, 0x74, 0x54, 0x61, 0x72, 0x67, 0x65, 0x74, 0x73, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x4d, 0x65, 0x74, 0x61, 0x4a, 0x53, 0x4f, 0x4e) o = msgp.AppendBytes(o, z.BucketTargetsConfigMetaJSON) + // string "CorsConfigXML" + o = append(o, 0xad, 0x43, 0x6f, 0x72, 0x73, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x58, 0x4d, 0x4c) + o = msgp.AppendBytes(o, z.CorsConfigXML) // string "PolicyConfigUpdatedAt" o = append(o, 0xb5, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74) o = msgp.AppendTime(o, z.PolicyConfigUpdatedAt) @@ -520,6 +555,9 @@ func (z *BucketMetadata) MarshalMsg(b []byte) (o []byte, err error) { // string "BucketTargetsConfigMetaUpdatedAt" o = append(o, 0xd9, 0x20, 0x42, 0x75, 0x63, 0x6b, 0x65, 0x74, 0x54, 0x61, 0x72, 0x67, 0x65, 0x74, 0x73, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x4d, 0x65, 0x74, 0x61, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74) o = msgp.AppendTime(o, z.BucketTargetsConfigMetaUpdatedAt) + // string "CorsConfigUpdatedAt" + o = append(o, 0xb3, 0x43, 0x6f, 0x72, 0x73, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74) + o = msgp.AppendTime(o, z.CorsConfigUpdatedAt) return } @@ -625,6 +663,12 @@ func (z *BucketMetadata) UnmarshalMsg(bts []byte) (o []byte, err error) { err = msgp.WrapError(err, "BucketTargetsConfigMetaJSON") return } + case "CorsConfigXML": + z.CorsConfigXML, bts, err = msgp.ReadBytesBytes(bts, z.CorsConfigXML) + if err != nil { + err = msgp.WrapError(err, "CorsConfigXML") + return + } case "PolicyConfigUpdatedAt": z.PolicyConfigUpdatedAt, bts, err = msgp.ReadTimeBytes(bts) if err != nil { @@ -691,6 +735,12 @@ func (z *BucketMetadata) UnmarshalMsg(bts []byte) (o []byte, err error) { err = msgp.WrapError(err, "BucketTargetsConfigMetaUpdatedAt") return } + case "CorsConfigUpdatedAt": + z.CorsConfigUpdatedAt, bts, err = msgp.ReadTimeBytes(bts) + if err != nil { + err = msgp.WrapError(err, "CorsConfigUpdatedAt") + return + } default: bts, err = msgp.Skip(bts) if err != nil { @@ -705,6 +755,6 @@ func (z *BucketMetadata) UnmarshalMsg(bts []byte) (o []byte, err error) { // Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message func (z *BucketMetadata) Msgsize() (s int) { - s = 3 + 5 + msgp.StringPrefixSize + len(z.Name) + 8 + msgp.TimeSize + 12 + msgp.BoolSize + 17 + msgp.BytesPrefixSize + len(z.PolicyConfigJSON) + 22 + msgp.BytesPrefixSize + len(z.NotificationConfigXML) + 19 + msgp.BytesPrefixSize + len(z.LifecycleConfigXML) + 20 + msgp.BytesPrefixSize + len(z.ObjectLockConfigXML) + 20 + msgp.BytesPrefixSize + len(z.VersioningConfigXML) + 20 + msgp.BytesPrefixSize + len(z.EncryptionConfigXML) + 17 + msgp.BytesPrefixSize + len(z.TaggingConfigXML) + 16 + msgp.BytesPrefixSize + len(z.QuotaConfigJSON) + 21 + msgp.BytesPrefixSize + len(z.ReplicationConfigXML) + 24 + msgp.BytesPrefixSize + len(z.BucketTargetsConfigJSON) + 28 + msgp.BytesPrefixSize + len(z.BucketTargetsConfigMetaJSON) + 22 + msgp.TimeSize + 26 + msgp.TimeSize + 26 + msgp.TimeSize + 23 + msgp.TimeSize + 21 + msgp.TimeSize + 27 + msgp.TimeSize + 26 + msgp.TimeSize + 25 + msgp.TimeSize + 28 + msgp.TimeSize + 29 + msgp.TimeSize + 34 + msgp.TimeSize + s = 3 + 5 + msgp.StringPrefixSize + len(z.Name) + 8 + msgp.TimeSize + 12 + msgp.BoolSize + 17 + msgp.BytesPrefixSize + len(z.PolicyConfigJSON) + 22 + msgp.BytesPrefixSize + len(z.NotificationConfigXML) + 19 + msgp.BytesPrefixSize + len(z.LifecycleConfigXML) + 20 + msgp.BytesPrefixSize + len(z.ObjectLockConfigXML) + 20 + msgp.BytesPrefixSize + len(z.VersioningConfigXML) + 20 + msgp.BytesPrefixSize + len(z.EncryptionConfigXML) + 17 + msgp.BytesPrefixSize + len(z.TaggingConfigXML) + 16 + msgp.BytesPrefixSize + len(z.QuotaConfigJSON) + 21 + msgp.BytesPrefixSize + len(z.ReplicationConfigXML) + 24 + msgp.BytesPrefixSize + len(z.BucketTargetsConfigJSON) + 28 + msgp.BytesPrefixSize + len(z.BucketTargetsConfigMetaJSON) + 14 + msgp.BytesPrefixSize + len(z.CorsConfigXML) + 22 + msgp.TimeSize + 26 + msgp.TimeSize + 26 + msgp.TimeSize + 23 + msgp.TimeSize + 21 + msgp.TimeSize + 27 + msgp.TimeSize + 26 + msgp.TimeSize + 25 + msgp.TimeSize + 28 + msgp.TimeSize + 29 + msgp.TimeSize + 34 + msgp.TimeSize + 20 + msgp.TimeSize return } diff --git a/cmd/bucket-metadata_test.go b/cmd/bucket-metadata_test.go new file mode 100644 index 000000000..70447bcb2 --- /dev/null +++ b/cmd/bucket-metadata_test.go @@ -0,0 +1,41 @@ +// Copyright (c) 2015-2026 MinIO, Inc. +// +// This file is part of MinIO Object Storage stack +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package cmd + +import "testing" + +func TestBucketMetadataCorsRoundTrip(t *testing.T) { + meta := newBucketMetadata("test-cors") + meta.CorsConfigXML = []byte(`*GET`) + meta.CorsConfigUpdatedAt = UTCNow() + + buf, err := meta.MarshalMsg(nil) + if err != nil { + t.Fatal(err) + } + var got BucketMetadata + if _, err := got.UnmarshalMsg(buf); err != nil { + t.Fatal(err) + } + if string(got.CorsConfigXML) != string(meta.CorsConfigXML) { + t.Fatalf("CorsConfigXML not preserved: %q", string(got.CorsConfigXML)) + } + if !got.CorsConfigUpdatedAt.Equal(meta.CorsConfigUpdatedAt) { + t.Fatalf("CorsConfigUpdatedAt not preserved") + } +} diff --git a/cmd/bucket-notification-handlers.go b/cmd/bucket-notification-handlers.go index c41823b4e..63293d515 100644 --- a/cmd/bucket-notification-handlers.go +++ b/cmd/bucket-notification-handlers.go @@ -26,7 +26,7 @@ import ( "github.com/minio/minio/internal/event" "github.com/minio/minio/internal/logger" "github.com/minio/mux" - "github.com/minio/pkg/v3/policy" + "github.com/pgsty/silo-pkg/v3/policy" ) const ( diff --git a/cmd/bucket-object-lock.go b/cmd/bucket-object-lock.go index d0ad85144..e5828e2dd 100644 --- a/cmd/bucket-object-lock.go +++ b/cmd/bucket-object-lock.go @@ -22,13 +22,15 @@ import ( "errors" "math" "net/http" + "strings" + "time" + "github.com/minio/minio/internal/amztime" "github.com/minio/minio/internal/auth" objectlock "github.com/minio/minio/internal/bucket/object/lock" - "github.com/minio/minio/internal/bucket/replication" xhttp "github.com/minio/minio/internal/http" "github.com/minio/minio/internal/logger" - "github.com/minio/pkg/v3/policy" + "github.com/pgsty/silo-pkg/v3/policy" ) // BucketObjectLockSys - map of bucket and retention configuration. @@ -150,7 +152,11 @@ func enforceRetentionBypassForDelete(ctx context.Context, r *http.Request, bucke } // https://docs.aws.amazon.com/AmazonS3/latest/dev/object-lock-overview.html#object-lock-retention-modes // If you try to delete objects protected by governance mode and have s3:BypassGovernanceRetention, the operation will succeed. - if checkRequestAuthType(ctx, r, policy.BypassGovernanceRetentionAction, bucket, object.ObjectName) != ErrNone { + if reqInfo := logger.GetReqInfo(ctx); reqInfo != nil { + reqInfo.BucketName = bucket + reqInfo.ObjectName = object.ObjectName + } + if authorizeRequest(ctx, r, policy.BypassGovernanceRetentionAction) != ErrNone { return errAuthentication } } @@ -198,7 +204,7 @@ func enforceRetentionBypassForPut(ctx context.Context, r *http.Request, oi Objec byPassSet, r, cred, owner) // Governance mode retention period cannot be shortened, if x-amz-bypass-governance is not set. if !byPassSet { - if objRetention.Mode != objectlock.RetGovernance || objRetention.RetainUntilDate.Before((ret.RetainUntilDate.Time)) { + if objRetention.Mode != objectlock.RetGovernance || objRetention.RetainUntilDate.Before(ret.RetainUntilDate.Time) { return ObjectLocked{Bucket: oi.Bucket, Object: oi.Name, VersionID: oi.VersionID} } } @@ -209,7 +215,7 @@ func enforceRetentionBypassForPut(ctx context.Context, r *http.Request, oi Objec case objectlock.RetCompliance: // Compliance retention mode cannot be changed or shortened. // https://docs.aws.amazon.com/AmazonS3/latest/dev/object-lock-overview.html#object-lock-retention-modes - if objRetention.Mode != objectlock.RetCompliance || objRetention.RetainUntilDate.Before((ret.RetainUntilDate.Time)) { + if objRetention.Mode != objectlock.RetCompliance || objRetention.RetainUntilDate.Before(ret.RetainUntilDate.Time) { return ObjectLocked{Bucket: oi.Bucket, Object: oi.Name, VersionID: oi.VersionID} } apiErr := isPutRetentionAllowed(oi.Bucket, oi.Name, @@ -242,7 +248,7 @@ func enforceRetentionBypassForPut(ctx context.Context, r *http.Request, oi Objec // For objects in "Compliance" mode, retention date cannot be shortened, and mode cannot be altered. // For objects with legal hold header set, the s3:PutObjectLegalHold permission is expected to be set // Both legal hold and retention can be applied independently on an object -func checkPutObjectLockAllowed(ctx context.Context, rq *http.Request, bucket, object string, getObjectInfoFn GetObjectInfoFn, retentionPermErr, legalHoldPermErr APIErrorCode) (objectlock.RetMode, objectlock.RetentionDate, objectlock.ObjectLegalHold, APIErrorCode) { +func checkPutObjectLockAllowed(ctx context.Context, rq *http.Request, bucket, object string, getObjectInfoFn GetObjectInfoFn, retentionPermErr, legalHoldPermErr APIErrorCode, replicaTrusted bool) (objectlock.RetMode, objectlock.RetentionDate, objectlock.ObjectLegalHold, APIErrorCode) { var mode objectlock.RetMode var retainDate objectlock.RetentionDate var legalHold objectlock.ObjectLegalHold @@ -269,9 +275,7 @@ func checkPutObjectLockAllowed(ctx context.Context, rq *http.Request, bucket, ob return mode, retainDate, legalHold, toAPIErrorCode(ctx, err) } - replica := rq.Header.Get(xhttp.AmzBucketReplicationStatus) == replication.Replica.String() - - if opts.VersionID != "" && !replica { + if opts.VersionID != "" && !replicaTrusted { if objInfo, err := getObjectInfoFn(ctx, bucket, object, opts); err == nil { r := objectlock.GetObjectRetentionMeta(objInfo.UserDefined) t, err := objectlock.UTCNowNTP() @@ -307,8 +311,8 @@ func checkPutObjectLockAllowed(ctx context.Context, rq *http.Request, bucket, ob if err != nil { return mode, retainDate, legalHold, toAPIErrorCode(ctx, err) } - rMode, rDate, err := objectlock.ParseObjectLockRetentionHeaders(rq.Header) - if err != nil && (!replica || rMode != "" || !rDate.IsZero()) { + rMode, rDate, err := objectlock.ParseObjectLockRetentionHeaders(rq.Header, replicaTrusted) + if err != nil && (!replicaTrusted || rMode != "" || !rDate.IsZero()) { return mode, retainDate, legalHold, toAPIErrorCode(ctx, err) } if retentionPermErr != ErrNone { @@ -316,7 +320,7 @@ func checkPutObjectLockAllowed(ctx context.Context, rq *http.Request, bucket, ob } return rMode, rDate, legalHold, ErrNone } - if replica { // replica inherits retention metadata only from source + if replicaTrusted { // replica inherits retention metadata only from source return "", objectlock.RetentionDate{}, legalHold, ErrNone } if !retentionRequested && retentionCfg.Validity > 0 { @@ -343,3 +347,177 @@ func checkPutObjectLockAllowed(ctx context.Context, rq *http.Request, bucket, ob func NewBucketObjectLockSys() *BucketObjectLockSys { return &BucketObjectLockSys{} } + +// objectLockState is the Object Lock metadata of a stored object version +// together with the replication timestamps that order updates to it. +type objectLockState struct { + mode, retainUntil, retentionTimestamp string + legalHold, legalHoldTimestamp string +} + +func storedObjectLockState(metadata map[string]string) objectLockState { + return objectLockState{ + mode: metadata[strings.ToLower(xhttp.AmzObjectLockMode)], + retainUntil: metadata[strings.ToLower(xhttp.AmzObjectLockRetainUntilDate)], + retentionTimestamp: metadata[ReservedMetadataPrefixLower+ObjectLockRetentionTimestamp], + legalHold: metadata[strings.ToLower(xhttp.AmzObjectLockLegalHold)], + legalHoldTimestamp: metadata[ReservedMetadataPrefixLower+ObjectLockLegalHoldTimestamp], + } +} + +// olderThan reports whether a stored replication timestamp is missing, +// unreadable, or earlier than the source timestamp, in which case the +// replica update wins. A zero source timestamp never wins. +func olderThan(stored string, src time.Time) bool { + if src.IsZero() { + return false + } + ondisk, err := time.Parse(time.RFC3339Nano, stored) + return err != nil || ondisk.Before(src) +} + +func (s objectLockState) retentionIsOlderThan(src time.Time) bool { + return olderThan(s.retentionTimestamp, src) +} + +func (s objectLockState) legalHoldIsOlderThan(src time.Time) bool { + return olderThan(s.legalHoldTimestamp, src) +} + +// restoreRetention and restoreLegalHold put the stored state back into +// metadata that was rebuilt from a request whose update was not applied. +func (s objectLockState) restoreRetention(metadata map[string]string) { + // The stored timestamp orders the next update and must survive even when + // the stored value is empty, which is how a removal is recorded. + if s.retentionTimestamp != "" { + metadata[ReservedMetadataPrefixLower+ObjectLockRetentionTimestamp] = s.retentionTimestamp + } + if s.mode == "" { + return + } + metadata[strings.ToLower(xhttp.AmzObjectLockMode)] = s.mode + metadata[strings.ToLower(xhttp.AmzObjectLockRetainUntilDate)] = s.retainUntil +} + +func (s objectLockState) restoreLegalHold(metadata map[string]string) { + if s.legalHoldTimestamp != "" { + metadata[ReservedMetadataPrefixLower+ObjectLockLegalHoldTimestamp] = s.legalHoldTimestamp + } + if s.legalHold == "" { + return + } + metadata[strings.ToLower(xhttp.AmzObjectLockLegalHold)] = s.legalHold +} + +// replicaStoredLock reads the Object Lock state stored on the addressed version +// so a trusted replica write can order its update against it. A missing object +// or version yields an empty state, which is correct for the first write of a +// version; any other read error is returned so the caller fails the write rather +// than ordering an incoming update against lock state it merely failed to read +// (an older incoming value must not win over a newer stored one just because the +// read timed out). +func replicaStoredLock(ctx context.Context, getObjectInfo GetObjectInfoFn, bucket, object, versionID string) (objectLockState, error) { + oi, err := getObjectInfo(ctx, bucket, object, ObjectOptions{VersionID: versionID}) + switch { + case err == nil: + return storedObjectLockState(oi.UserDefined), nil + case isErrObjectNotFound(err) || isErrVersionNotFound(err): + return objectLockState{}, nil + default: + return objectLockState{}, err + } +} + +// applyReplicatedObjectLock writes the retention and legal-hold decision into +// metadata for a PUT, CopyObject, or multipart-initiation request. A request +// that is not an actual trusted replica -- a normal user write, or a trusted +// peer that carried the replication marker without REPLICA status -- takes +// ordinary write semantics: a validated value is applied and stamped now, and a +// missing value is left as is. Only an actual replica update is ordered against +// the state already stored on the addressed version, so a stale value cannot +// overwrite a newer one and a full retransmit cannot roll a destination back. +// The stored argument is meaningful only for a replica; callers pass an empty +// state otherwise. Only the two Object Lock keys and their reserved ordering +// timestamps are touched; any encryption-metadata reconciliation stays with the +// caller. +func applyReplicatedObjectLock(metadata map[string]string, stored objectLockState, + replicaTrusted bool, + retentionMode objectlock.RetMode, retentionDate objectlock.RetentionDate, + legalHold objectlock.ObjectLegalHold, srcRetentionTimestamp, srcLegalholdTimestamp time.Time, +) { + switch { + case !replicaTrusted: + // Ordinary write semantics: apply a validated retention and stamp it now; + // a missing value carries no instruction, so leave the metadata as it is. + if retentionMode.Valid() { + metadata[strings.ToLower(xhttp.AmzObjectLockMode)] = string(retentionMode) + metadata[strings.ToLower(xhttp.AmzObjectLockRetainUntilDate)] = amztime.ISO8601Format(retentionDate.UTC()) + metadata[ReservedMetadataPrefixLower+ObjectLockRetentionTimestamp] = UTCNow().Format(time.RFC3339Nano) + } + case !stored.retentionIsOlderThan(srcRetentionTimestamp): + // The stored update is at least as new as this replica's, or the replica + // carries no ordering timestamp: keep what is stored. This is also how a + // stale retransmit is rejected. + stored.restoreRetention(metadata) + default: + // The replica update wins. A removal carries no value but still records + // the source timestamp that orders it. + if retentionMode.Valid() { + metadata[strings.ToLower(xhttp.AmzObjectLockMode)] = string(retentionMode) + metadata[strings.ToLower(xhttp.AmzObjectLockRetainUntilDate)] = amztime.ISO8601Format(retentionDate.UTC()) + } + metadata[ReservedMetadataPrefixLower+ObjectLockRetentionTimestamp] = srcRetentionTimestamp.UTC().Format(time.RFC3339Nano) + } + + // Legal hold has no removal in S3: an explicitly empty header is already + // rejected as an invalid status, so the only value-less shape that gets here + // is an absent one, which conveys no legal-hold change. Only a valid status + // can win. + switch { + case !replicaTrusted: + if legalHold.Status.Valid() { + metadata[strings.ToLower(xhttp.AmzObjectLockLegalHold)] = string(legalHold.Status) + metadata[ReservedMetadataPrefixLower+ObjectLockLegalHoldTimestamp] = UTCNow().Format(time.RFC3339Nano) + } + case legalHold.Status.Valid() && stored.legalHoldIsOlderThan(srcLegalholdTimestamp): + metadata[strings.ToLower(xhttp.AmzObjectLockLegalHold)] = string(legalHold.Status) + metadata[ReservedMetadataPrefixLower+ObjectLockLegalHoldTimestamp] = srcLegalholdTimestamp.UTC().Format(time.RFC3339Nano) + default: + stored.restoreLegalHold(metadata) + } +} + +// reconcileStoredObjectLock re-orders the Object Lock already written into +// metadata against the state currently stored on the destination version, both +// compared by their reserved ordering timestamps. It runs inside the object +// layer under the namespace write lock that guards the version replacement, +// after the destination version is read and before the new one is committed, so +// a replica update whose ordering was decided at handler time (or, for multipart, +// at initiation) cannot overwrite a newer lock update that reached the version in +// between. metadata already carries the incoming update with its source +// timestamps; a stored value that is not older than the incoming one is put back, +// which for a stored removal means clearing the incoming value and keeping only +// the removal's timestamp. Only the two lock keys and their reserved timestamps +// move; a non-replica write never sets the flag that invokes this. +func reconcileStoredObjectLock(metadata map[string]string, stored objectLockState) { + incoming := storedObjectLockState(metadata) + + incomingRetentionTS, _ := time.Parse(time.RFC3339Nano, incoming.retentionTimestamp) + if !stored.retentionIsOlderThan(incomingRetentionTS) { + // The stored retention is at least as new as the incoming one (or the + // incoming update is unordered): drop the incoming value and put the stored + // state back, which may itself be a removal (value keys absent, timestamp + // present). + delete(metadata, strings.ToLower(xhttp.AmzObjectLockMode)) + delete(metadata, strings.ToLower(xhttp.AmzObjectLockRetainUntilDate)) + delete(metadata, ReservedMetadataPrefixLower+ObjectLockRetentionTimestamp) + stored.restoreRetention(metadata) + } + + incomingLegalHoldTS, _ := time.Parse(time.RFC3339Nano, incoming.legalHoldTimestamp) + if incoming.legalHold == "" || !stored.legalHoldIsOlderThan(incomingLegalHoldTS) { + delete(metadata, strings.ToLower(xhttp.AmzObjectLockLegalHold)) + delete(metadata, ReservedMetadataPrefixLower+ObjectLockLegalHoldTimestamp) + stored.restoreLegalHold(metadata) + } +} diff --git a/cmd/bucket-policy-handlers.go b/cmd/bucket-policy-handlers.go index 994b0b0da..20d5afe61 100644 --- a/cmd/bucket-policy-handlers.go +++ b/cmd/bucket-policy-handlers.go @@ -27,7 +27,7 @@ import ( "github.com/minio/madmin-go/v3" "github.com/minio/minio/internal/logger" "github.com/minio/mux" - "github.com/minio/pkg/v3/policy" + "github.com/pgsty/silo-pkg/v3/policy" ) const ( diff --git a/cmd/bucket-policy-handlers_test.go b/cmd/bucket-policy-handlers_test.go index e506aceb0..92a93cbb6 100644 --- a/cmd/bucket-policy-handlers_test.go +++ b/cmd/bucket-policy-handlers_test.go @@ -29,8 +29,8 @@ import ( "testing" "github.com/minio/minio/internal/auth" - "github.com/minio/pkg/v3/policy" - "github.com/minio/pkg/v3/policy/condition" + "github.com/pgsty/silo-pkg/v3/policy" + "github.com/pgsty/silo-pkg/v3/policy/condition" ) func getAnonReadOnlyBucketPolicy(bucketName string) *policy.BucketPolicy { diff --git a/cmd/bucket-policy.go b/cmd/bucket-policy.go index 84c15e54a..35c7a0f22 100644 --- a/cmd/bucket-policy.go +++ b/cmd/bucket-policy.go @@ -33,8 +33,8 @@ import ( "github.com/minio/minio/internal/handlers" xhttp "github.com/minio/minio/internal/http" "github.com/minio/minio/internal/logger" - "github.com/minio/pkg/v3/policy" - "github.com/minio/pkg/v3/policy/condition" + "github.com/pgsty/silo-pkg/v3/policy" + "github.com/pgsty/silo-pkg/v3/policy/condition" ) // PolicySys - policy subsystem. diff --git a/cmd/bucket-policy_test.go b/cmd/bucket-policy_test.go index 080848799..7bd5bb824 100644 --- a/cmd/bucket-policy_test.go +++ b/cmd/bucket-policy_test.go @@ -29,8 +29,8 @@ import ( "github.com/minio/minio/internal/auth" "github.com/minio/minio/internal/handlers" xhttp "github.com/minio/minio/internal/http" - "github.com/minio/pkg/v3/policy" - "github.com/minio/pkg/v3/policy/condition" + "github.com/pgsty/silo-pkg/v3/policy" + "github.com/pgsty/silo-pkg/v3/policy/condition" ) const ( diff --git a/cmd/bucket-quota.go b/cmd/bucket-quota.go index d9779c21a..f515a86b2 100644 --- a/cmd/bucket-quota.go +++ b/cmd/bucket-quota.go @@ -43,6 +43,17 @@ func NewBucketQuotaSys() *BucketQuotaSys { return &BucketQuotaSys{} } +// getBucketQuotaSize returns the effective enforced hard-quota size. +func getBucketQuotaSize(quota *madmin.BucketQuota) uint64 { + if quota == nil || quota.Type != madmin.HardQuota { + return 0 + } + if quota.Size > 0 { + return quota.Size + } + return quota.Quota +} + var bucketStorageCache = cachevalue.New[DataUsageInfo]() // Init initialize bucket quota. @@ -110,14 +121,7 @@ func (sys *BucketQuotaSys) enforceQuotaHard(ctx context.Context, bucket string, return err } - var quotaSize uint64 - if q != nil && q.Type == madmin.HardQuota { - if q.Size > 0 { - quotaSize = q.Size - } else if q.Quota > 0 { - quotaSize = q.Quota - } - } + quotaSize := getBucketQuotaSize(q) if quotaSize > 0 { if uint64(size) >= quotaSize { // check if file size already exceeds the quota return BucketQuotaExceeded{Bucket: bucket} diff --git a/cmd/bucket-quota_test.go b/cmd/bucket-quota_test.go new file mode 100644 index 000000000..bdcefbb8e --- /dev/null +++ b/cmd/bucket-quota_test.go @@ -0,0 +1,77 @@ +// Copyright (c) 2015-2025 MinIO, Inc. +// Copyright (c) 2025-2026 PGSTY +// +// This file is part of MinIO Object Storage stack +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package cmd + +import ( + "testing" + + "github.com/minio/madmin-go/v3" +) + +func TestGetBucketQuotaSize(t *testing.T) { + tests := []struct { + name string + quota *madmin.BucketQuota + want uint64 + }{ + {name: "nil"}, + {name: "empty", quota: &madmin.BucketQuota{}}, + {name: "current size", quota: &madmin.BucketQuota{Type: madmin.HardQuota, Size: 1024}, want: 1024}, + {name: "legacy quota", quota: &madmin.BucketQuota{Type: madmin.HardQuota, Quota: 2048}, want: 2048}, + {name: "size takes precedence", quota: &madmin.BucketQuota{Type: madmin.HardQuota, Size: 1024, Quota: 2048}, want: 1024}, + {name: "missing type", quota: &madmin.BucketQuota{Size: 1024}}, + {name: "unsupported type", quota: &madmin.BucketQuota{Type: "fifo", Size: 1024}}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := getBucketQuotaSize(tt.quota); got != tt.want { + t.Fatalf("getBucketQuotaSize() = %d, want %d", got, tt.want) + } + }) + } +} + +func TestIsBktQuotaCfgReplicated(t *testing.T) { + hardQuota := func(size, legacy uint64) *madmin.BucketQuota { + return &madmin.BucketQuota{Type: madmin.HardQuota, Size: size, Quota: legacy} + } + + tests := []struct { + name string + quotas []*madmin.BucketQuota + want bool + }{ + {name: "none configured", quotas: []*madmin.BucketQuota{nil, nil}, want: true}, + {name: "missing from one site", quotas: []*madmin.BucketQuota{hardQuota(1024, 0), nil}}, + {name: "matching size", quotas: []*madmin.BucketQuota{hardQuota(1024, 0), hardQuota(1024, 0)}, want: true}, + {name: "different size", quotas: []*madmin.BucketQuota{hardQuota(1024, 0), hardQuota(2048, 0)}}, + {name: "equivalent representations", quotas: []*madmin.BucketQuota{hardQuota(1024, 0), hardQuota(0, 1024)}, want: true}, + {name: "different typeless size", quotas: []*madmin.BucketQuota{{Size: 1024}, {Size: 2048}}}, + {name: "different type", quotas: []*madmin.BucketQuota{hardQuota(1024, 0), {Type: "fifo", Size: 1024}}}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := isBktQuotaCfgReplicated(len(tt.quotas), tt.quotas); got != tt.want { + t.Fatalf("isBktQuotaCfgReplicated() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/cmd/bucket-replication-handlers.go b/cmd/bucket-replication-handlers.go index 05a9d2e85..29e686265 100644 --- a/cmd/bucket-replication-handlers.go +++ b/cmd/bucket-replication-handlers.go @@ -34,7 +34,7 @@ import ( xhttp "github.com/minio/minio/internal/http" "github.com/minio/minio/internal/logger" "github.com/minio/mux" - "github.com/minio/pkg/v3/policy" + "github.com/pgsty/silo-pkg/v3/policy" ) // PutBucketReplicationConfigHandler - PUT Bucket replication configuration. @@ -617,7 +617,7 @@ func (api objectAPIHandlers) ValidateBucketReplicationCredsHandler(w http.Respon ReplicationValidityCheck: true, // set this to validate the replication config }, } - obj := path.Join(minioReservedBucket, globalLocalNodeNameHex, "deleteme") + obj := replicationValidationObject(rule) ui, err := c.PutObject(ctx, clnt.Bucket, obj, reader, int64(len(buf)), "", "", putOpts) if err != nil && !isReplicationPermissionCheck(ErrorRespToObjectError(err, bucket, obj)) { writeErrorResponse(ctx, w, errorCodes.ToAPIErrWithErr(ErrReplicationValidationError, fmt.Errorf("s3:ReplicateObject permissions missing for replication user: %w", err)), r.URL) @@ -658,3 +658,7 @@ func (api objectAPIHandlers) ValidateBucketReplicationCredsHandler(w http.Respon // Write success response. writeSuccessResponseHeadersOnly(w) } + +func replicationValidationObject(rule replication.Rule) string { + return path.Join(rule.Prefix(), minioReservedBucket, globalLocalNodeNameHex, "deleteme") +} diff --git a/cmd/bucket-replication.go b/cmd/bucket-replication.go index 5f6268cb6..def8a74f8 100644 --- a/cmd/bucket-replication.go +++ b/cmd/bucket-replication.go @@ -418,7 +418,12 @@ func checkReplicateDelete(ctx context.Context, bucket string, dobj ObjectToDelet // target cluster, the object version is marked deleted on the source and hidden from listing. It is permanently // deleted from the source when the VersionPurgeStatus changes to "Complete", i.e after replication succeeds // on target. -func replicateDelete(ctx context.Context, dobj DeletedObjectReplicationInfo, objectAPI ObjectLayer) { +// replicateDelete replicates a delete (delete marker or version purge) to all +// applicable targets and returns the per-target replication outcome. Callers +// that only trigger replication may ignore the return value; the resync path +// uses it to classify success/failure per target rather than inferring it from +// the mere presence or absence of the target version. +func replicateDelete(ctx context.Context, dobj DeletedObjectReplicationInfo, objectAPI ObjectLayer) replicatedInfos { var replicationStatus replication.StatusType bucket := dobj.Bucket versionID := dobj.DeleteMarkerVersionID @@ -453,7 +458,7 @@ func replicateDelete(ctx context.Context, dobj DeletedObjectReplicationInfo, obj Host: globalLocalNodeName, EventName: event.ObjectReplicationNotTracked, }) - return + return replicatedInfos{} } dsc, err := parseReplicateDecision(ctx, bucket, dobj.ReplicationState.ReplicateDecisionStr) if err != nil { @@ -471,7 +476,7 @@ func replicateDelete(ctx context.Context, dobj DeletedObjectReplicationInfo, obj Host: globalLocalNodeName, EventName: event.ObjectReplicationNotTracked, }) - return + return replicatedInfos{} } // Lock the object name before starting replication operation. @@ -492,7 +497,7 @@ func replicateDelete(ctx context.Context, dobj DeletedObjectReplicationInfo, obj Host: globalLocalNodeName, EventName: event.ObjectReplicationNotTracked, }) - return + return replicatedInfos{} } ctx = lkctx.Context() defer lk.Unlock(lkctx) @@ -597,6 +602,7 @@ func replicateDelete(ctx context.Context, dobj DeletedObjectReplicationInfo, obj EventName: eventName, }) } + return rinfos } func replicateDeleteToTarget(ctx context.Context, dobj DeletedObjectReplicationInfo, tgt *TargetClient) (rinfo replicatedTargetInfo) { @@ -779,6 +785,15 @@ func putReplicationOpts(ctx context.Context, sc string, objInfo ObjectInfo) (put meta := make(map[string]string) isSSEC := crypto.SSEC.IsEncrypted(objInfo.UserDefined) + // An SSE-C object is replicated as raw ciphertext, and the replication + // headers carry no compression state. Sending a compressed SSE-C object + // would land a replica that decrypts to an S2 stream instead of the + // object, so fail loudly instead of writing a wrong replica. + if isSSEC && objInfo.IsCompressed() { + return putOpts, false, fmt.Errorf("replication of a compressed SSE-C object is not supported: %s/%s(%s)", + objInfo.Bucket, objInfo.Name, objInfo.VersionID) + } + for k, v := range objInfo.UserDefined { _, isValidSSEHeader := validSSEReplicationHeaders[k] // In case of SSE-C objects copy the allowed internal headers as well @@ -857,19 +872,29 @@ func putReplicationOpts(ctx context.Context, sc string, objInfo ObjectInfo) (put if cc, ok := lkMap.Lookup(xhttp.CacheControl); ok { putOpts.CacheControl = cc } - if mode, ok := lkMap.Lookup(xhttp.AmzObjectLockMode); ok { - rmode := minio.RetentionMode(mode) - putOpts.Mode = rmode + mode, hasMode := lkMap.Lookup(xhttp.AmzObjectLockMode) + retainDateStr, hasRetainDate := lkMap.Lookup(xhttp.AmzObjectLockRetainUntilDate) + if hasMode { + putOpts.Mode = minio.RetentionMode(mode) } - if retainDateStr, ok := lkMap.Lookup(xhttp.AmzObjectLockRetainUntilDate); ok { + // A removed retention is stored as an empty or absent mode and date; it is + // sent as a value-less update that still carries its ordering timestamp. + if hasRetainDate && retainDateStr != "" { rdate, err := amztime.ISO8601Parse(retainDateStr) if err != nil { return putOpts, false, err } putOpts.RetainUntilDate = rdate - // set retention timestamp in opts + } + retainTmstampStr, hasRetainTmstamp := objInfo.UserDefined[ReservedMetadataPrefixLower+ObjectLockRetentionTimestamp] + if hasMode || hasRetainDate || hasRetainTmstamp { + // Send the ordering timestamp whenever the version carries one, even for a + // removal whose value keys are absent (the shape a retransmit PUT leaves), + // so the next hop can order the removal instead of keeping obsolete + // retention. retTimestamp := objInfo.ModTime - if retainTmstampStr, ok := objInfo.UserDefined[ReservedMetadataPrefixLower+ObjectLockRetentionTimestamp]; ok { + if hasRetainTmstamp { + var err error retTimestamp, err = time.Parse(time.RFC3339Nano, retainTmstampStr) if err != nil { return putOpts, false, err @@ -931,11 +956,17 @@ func equals(k1 string, keys ...string) bool { return false } +// nullVersionExcludedFromResync reports the exclusion at the head of getReplicationAction, kept +// verbatim from upstream: an existing object resync leaves a null version alone when the source +// modification time is later than the one the target reports, without comparing anything else. +func nullVersionExcludedFromResync(oi1 ObjectInfo, oi2 minio.ObjectInfo, opType replication.Type) bool { + return opType == replication.ExistingObjectReplicationType && + oi1.ModTime.Unix() > oi2.LastModified.Unix() && oi1.VersionID == nullVersionID +} + // returns replicationAction by comparing metadata between source and target func getReplicationAction(oi1 ObjectInfo, oi2 minio.ObjectInfo, opType replication.Type) replicationAction { - // Avoid resyncing null versions created prior to enabling replication if target has a newer copy - if opType == replication.ExistingObjectReplicationType && - oi1.ModTime.Unix() > oi2.LastModified.Unix() && oi1.VersionID == nullVersionID { + if nullVersionExcludedFromResync(oi1, oi2, opType) { return replicateNone } sz, _ := oi1.GetActualSize() @@ -986,9 +1017,21 @@ func getReplicationAction(oi1 ObjectInfo, oi2 minio.ObjectInfo, opType replicati "X-Amz-Meta-", } + // An empty object lock mode or retain-until-date records a removed retention, but + // it is omitted from GET/HEAD response headers: setObjectHeaders() skips both keys + // when the value is empty, and FilterObjectLockMetadata() drops them when the mode + // is not valid. The target can therefore never report them, so treat empty and + // absent as equal rather than as a permanent difference. + emptyLockValue := func(k, v string) bool { + return v == "" && equals(k, xhttp.AmzObjectLockMode, xhttp.AmzObjectLockRetainUntilDate) + } + // compare metadata on both maps to see if meta is identical compareMeta1 := make(map[string]string) for k, v := range oi1.UserDefined { + if emptyLockValue(k, v) { + continue + } var found bool for _, prefix := range compareKeys { if !stringsHasPrefixFold(k, prefix) { @@ -1004,6 +1047,10 @@ func getReplicationAction(oi1 ObjectInfo, oi2 minio.ObjectInfo, opType replicati compareMeta2 := make(map[string]string) for k, v := range oi2.Metadata { + val := strings.Join(v, ",") + if emptyLockValue(k, val) { + continue + } var found bool for _, prefix := range compareKeys { if !stringsHasPrefixFold(k, prefix) { @@ -1013,7 +1060,7 @@ func getReplicationAction(oi1 ObjectInfo, oi2 minio.ObjectInfo, opType replicati break } if found { - compareMeta2[strings.ToLower(k)] = strings.Join(v, ",") + compareMeta2[strings.ToLower(k)] = val } } @@ -1024,9 +1071,87 @@ func getReplicationAction(oi1 ObjectInfo, oi2 minio.ObjectInfo, opType replicati return replicateNone } +// objectRetentionGetter is the part of the replication target client used to confirm whether a +// destination version still holds Object Lock retention. +type objectRetentionGetter interface { + GetObjectRetention(ctx context.Context, bucketName, objectName, versionID string) (*minio.RetentionMode, *time.Time, error) +} + +// retentionRemovedAtSource reports whether oi carries the shape a removed retention leaves behind. +// Two representations persist. A retention removed directly on this cluster keeps the object lock +// keys present with empty values (PutObjectRetentionHandler, cmd/object-handlers.go:3309-3316). A +// removal that arrived by replication keeps only the retention ordering timestamp, with the mode +// and retain-until-date keys absent, because restoreRetention and the replica update path write +// the timestamp alone when the mode is empty (cmd/bucket-object-lock.go:388-399, +// cmd/object-handlers.go:1782-1797). A present ordering timestamp paired with a non-empty mode is +// a retention that was set, not removed, and must not be mistaken for one. +func retentionRemovedAtSource(oi ObjectInfo) bool { + lkMap := caseInsensitiveMap(oi.UserDefined) + // Representation (1): an object lock key is present with an empty value. + for _, k := range []string{xhttp.AmzObjectLockMode, xhttp.AmzObjectLockRetainUntilDate} { + if v, ok := lkMap.Lookup(k); ok && v == "" { + return true + } + } + // Representation (2): a recorded retention ordering timestamp with the mode value absent or + // empty is a removal restoreRetention persisted without the empty public keys. + if _, ok := oi.UserDefined[ReservedMetadataPrefixLower+ObjectLockRetentionTimestamp]; ok { + if v, ok := lkMap.Lookup(xhttp.AmzObjectLockMode); !ok || v == "" { + return true + } + } + return false +} + +// targetRetentionConfirmedAbsent reports whether the destination version is known to hold no +// retention. A HEAD response omits retention both when the version has none and when the +// replication credential lacks s3:GetObjectRetention (cmd/object-handlers.go:942-946), so the +// comparison in getReplicationAction on its own cannot tell a removal that is already in sync from +// one the destination still holds. Only NoSuchObjectLockConfiguration, the answer for a version +// that carries no retention, and a response naming no retention mode count as absent. Everything +// else is uncertainty and is treated as still present, so that the removal is resent exactly as it +// is today: a denied or unreachable destination, a mode the SDK returned without recognizing since +// it does not validate it, and InvalidRequest, which names a bucket with no Object Lock +// configuration but is also what a destination answers when its own read of that configuration +// fails (cmd/bucket-object-lock.go:39-50 returns an error with a zero Retention, discarded at +// cmd/object-handlers.go:3275). +func targetRetentionConfirmedAbsent(ctx context.Context, tgt objectRetentionGetter, bucket, object, versionID string) bool { + mode, _, err := tgt.GetObjectRetention(ctx, bucket, object, versionID) + if err != nil { + return minio.ToErrorResponse(err).Code == "NoSuchObjectLockConfiguration" + } + // An absent or empty mode is no retention. A non-empty mode is retention, whether or not this + // SDK recognizes it. + return mode == nil || *mode == "" +} + +// replicationActionForTarget returns the action for a source version against a destination that +// answered HEAD. It is getReplicationAction plus the confirmation that a removed retention which +// compares as in sync really is: see targetRetentionConfirmedAbsent. +func replicationActionForTarget(ctx context.Context, oi1 ObjectInfo, oi2 minio.ObjectInfo, opType replication.Type, tgt objectRetentionGetter, bucket, object string) replicationAction { + rAction := getReplicationAction(oi1, oi2, opType) + if rAction != replicateNone || !retentionRemovedAtSource(oi1) { + return rAction + } + // A null version the resync deliberately leaves alone is not a comparison result, so it is + // not the confirmation's to reopen. + if nullVersionExcludedFromResync(oi1, oi2, opType) { + return rAction + } + if targetRetentionConfirmedAbsent(ctx, tgt, bucket, object, oi1.VersionID) { + return rAction + } + return replicateMetadata +} + // replicateObject replicates the specified version of the object to destination bucket // The source object is then updated to reflect the replication status. -func replicateObject(ctx context.Context, ri ReplicateObjectInfo, objectAPI ObjectLayer) { +// replicateObject replicates a single object version to all applicable targets +// and returns the per-target replication outcome. Callers that only trigger +// replication may ignore the return value; the resync path uses it to classify +// success/failure per target rather than inferring it from the mere existence +// of the target version. +func replicateObject(ctx context.Context, ri ReplicateObjectInfo, objectAPI ObjectLayer) replicatedInfos { var replicationStatus replication.StatusType defer func() { if replicationStatus.Empty() { @@ -1059,7 +1184,7 @@ func replicateObject(ctx context.Context, ri ReplicateObjectInfo, objectAPI Obje UserAgent: "Internal: [Replication]", Host: globalLocalNodeName, }) - return + return replicatedInfos{} } tgtArns := cfg.FilterTargetArns(replication.ObjectOpts{ Name: object, @@ -1079,7 +1204,7 @@ func replicateObject(ctx context.Context, ri ReplicateObjectInfo, objectAPI Obje Host: globalLocalNodeName, }) globalReplicationPool.Get().queueMRFSave(ri.ToMRFEntry()) - return + return replicatedInfos{} } ctx = lkctx.Context() defer lk.Unlock(lkctx) @@ -1185,6 +1310,7 @@ func replicateObject(ctx context.Context, ri ReplicateObjectInfo, objectAPI Obje ri.RetryCount++ globalReplicationPool.Get().queueMRFSave(ri.ToMRFEntry()) } + return rinfos } // replicateObject replicates object data for specified version of the object to destination bucket @@ -1299,6 +1425,8 @@ func (ri ReplicateObjectInfo) replicateObject(ctx context.Context, objectAPI Obj putOpts, isMP, err := putReplicationOpts(ctx, tgt.StorageClass, objInfo) if err != nil { + rinfo.Err = err + rinfo.ReplicationStatus = replication.Failed replLogIf(ctx, fmt.Errorf("failure setting options for replication bucket:%s err:%w", bucket, err)) sendEvent(eventArgs{ EventName: event.ObjectReplicationNotTracked, @@ -1467,7 +1595,7 @@ func (ri ReplicateObjectInfo) replicateAll(ctx context.Context, objectAPI Object sOpts.Set(xhttp.AmzTagDirective, "ACCESS") oi, cerr := tgt.StatObject(ctx, tgt.Bucket, object, sOpts) if cerr == nil { - rAction = getReplicationAction(objInfo, oi, ri.OpType) + rAction = replicationActionForTarget(ctx, objInfo, oi, ri.OpType, tgt, tgt.Bucket, object) rinfo.ReplicationStatus = replication.Completed if rAction == replicateNone { if ri.OpType == replication.ExistingObjectReplicationType && @@ -1497,11 +1625,14 @@ func (ri ReplicateObjectInfo) replicateAll(ctx context.Context, objectAPI Object return rinfo } } else { - // SSEC objects will refuse HeadObject without the decryption key. - // Ignore the error, since we know the object exists and versioning prevents overwriting existing versions. + // The sender holds no customer key, so the target refuses HeadObject on + // an SSE-C object and the replica cannot be compared. The metadata-only + // CopyObject that a replicateMetadata action would run then fails on any + // non-empty object, because the undecryptable source checksum makes the + // target recompute one and rewrite the data. A full retransmit is the + // only action that completes. if isSSEC && strings.Contains(cerr.Error(), errorCodes[ErrSSEEncryptedObject].Description) { - rinfo.ReplicationStatus = replication.Completed - rinfo.ReplicationAction = replicateNone + rAction = replicateAll goto applyAction } // if target returns error other than NoSuchKey, defer replication attempt @@ -1586,6 +1717,11 @@ applyAction: } else { putOpts, isMP, err := putReplicationOpts(ctx, tgt.StorageClass, objInfo) if err != nil { + // rinfo was primed Completed above; a failure to build the write + // options means nothing reached the target, so mark it Failed and + // carry the error instead of reporting a phantom success. + rinfo.ReplicationStatus = replication.Failed + rinfo.Err = err replLogIf(ctx, fmt.Errorf("failed to set replicate options for object %s/%s(%s) (target %s) err:%w", bucket, objInfo.Name, objInfo.VersionID, tgt.EndpointURL(), err)) sendEvent(eventArgs{ EventName: event.ObjectReplicationNotTracked, @@ -2873,6 +3009,150 @@ func (s *replicationResyncer) incStats(ts TargetReplicationResyncStatus, opts re s.statusMap[opts.bucket] = m } +// resyncResults consumes the per-object outcomes produced by the resync worker +// pool and applies each to the in-memory resync status via apply. It centralizes +// the finalization ordering so a status persisted after finish() returns always +// reflects every result. +type resyncResults struct { + ch chan TargetReplicationResyncStatus + apply func(TargetReplicationResyncStatus) + wg sync.WaitGroup +} + +// newResyncResults starts the result-consuming goroutine that folds each worker +// result into the bucket's resync status. +func (s *replicationResyncer) newResyncResults(opts resyncOpts) *resyncResults { + return startResyncResults(func(r TargetReplicationResyncStatus) { + s.incStats(r, opts) + globalSiteResyncMetrics.updateMetric(r, opts.resyncID) + }) +} + +// startResyncResults starts a goroutine that applies every received result with +// apply. Injecting the apply action keeps the shutdown ordering in finish() +// testable. +func startResyncResults(apply func(TargetReplicationResyncStatus)) *resyncResults { + rr := &resyncResults{ + ch: make(chan TargetReplicationResyncStatus, 1), + apply: apply, + } + rr.wg.Add(1) + go func() { + defer rr.wg.Done() + for r := range rr.ch { + rr.apply(r) + } + }() + return rr +} + +// finish shuts the resync pipeline down in an order that guarantees a status +// persisted afterwards reflects every result. It first closes the worker input +// channels and waits for the producer workers to exit, so none can send on a +// closed result channel (a hazard on early-return paths) and every submitted +// result is delivered (a result a worker discards on cancellation is +// intentionally not); only then does it close the result channel and wait for +// the consumer to apply the last buffered result. +func (rr *resyncResults) finish(workers []chan ReplicateObjectInfo, workerWg *sync.WaitGroup) { + for i := range workers { + xioutil.SafeClose(workers[i]) + } + workerWg.Wait() + xioutil.SafeClose(rr.ch) + rr.wg.Wait() +} + +// sendResyncResult delivers a worker's computed per-object result to ch, +// returning false if the worker must stop first. On the resync-cancel signal it +// records the abort - the already-computed result is dropped - so +// finalResyncStatus can downgrade a Completed run; on ctx cancellation it stops +// without recording, since finalResyncStatus's parent-context check covers that. +func (s *replicationResyncer) sendResyncResult(ctx context.Context, ch chan<- TargetReplicationResyncStatus, st TargetReplicationResyncStatus, workerAborted *atomic.Bool) bool { + select { + case <-ctx.Done(): + return false + case <-s.resyncCancelCh: + workerAborted.Store(true) + return false + case ch <- st: + return true + } +} + +// finalResyncStatus downgrades a Completed status to Failed when the run could +// not have observed every object: the parent context was canceled (workers then +// return without sending their computed result) or a worker dropped a result on +// the resync-cancel signal. Without this a persisted Completed would misrepresent +// an incomplete resync. +func finalResyncStatus(status ResyncStatusType, ctxErr error, workerAborted bool) ResyncStatusType { + if status == ResyncCompleted && (ctxErr != nil || workerAborted) { + return ResyncFailed + } + return status +} + +// resyncTargetSucceeded reports whether this object (or delete) actually +// replicated to the target, from the target's own outcome. A version purge +// reports success through VersionPurgeStatus, not ReplicationStatus. For an +// object or delete marker, success requires a Completed status; a retained +// error is a real failure unless it is the benign duplicate 412 the +// destination returns when it already holds this exact ETag and version, which +// replicateAll deliberately keeps Completed. +func resyncTargetSucceeded(t replicatedTargetInfo, roi ReplicateObjectInfo) bool { + if !roi.VersionPurgeStatus.Empty() { + return t.VersionPurgeStatus == replication.VersionPurgeComplete + } + if t.ReplicationStatus != replication.Completed { + return false + } + return t.Err == nil || minio.ToErrorResponse(t.Err).Code == "PreconditionFailed" +} + +// resyncResultFor derives the resync outcome for target arn from the aggregate +// replication result of a single object (or delete). The target counts as a +// success only when its own replication Completed without error - not when the +// target version merely exists. A target that Failed, errored, or was not +// attempted for this object (its arn absent from the result) counts as a +// failure, and the failed byte count is recorded (previously always zero). +func resyncResultFor(rinfos replicatedInfos, arn string, roi ReplicateObjectInfo) TargetReplicationResyncStatus { + st := TargetReplicationResyncStatus{Object: roi.Name, Bucket: roi.Bucket} + for _, t := range rinfos.Targets { + if t.Arn != arn { + continue + } + if resyncTargetSucceeded(t, roi) { + sz := t.Size + if sz == 0 { + sz = roi.Size + } + st.ReplicatedCount++ + st.ReplicatedSize += sz + } else { + st.FailedCount++ + st.FailedSize += roi.Size + } + return st + } + // arn was not attempted for this object: a resync that cannot confirm the + // object reached the target is not a success. + st.FailedCount++ + st.FailedSize += roi.Size + return st +} + +// objectNeedsResyncForARN reports whether roi must be resynced for target arn +// specifically. The resync worker pool is scoped to a single target (opts.arn), +// so an object that only qualifies for a different target must be skipped here: +// admitting it would replicate it for arn's peers only, leaving arn absent from +// the per-object result, which resyncResultFor then (correctly, but +// misleadingly) counts as a failure for arn - an object arn was never +// responsible for. Only opts.arn carries this resync's ResetID, and that reset +// is already folded into its per-target decision, so the per-target check both +// scopes dispatch and honors the reset. +func objectNeedsResyncForARN(roi ReplicateObjectInfo, arn string) bool { + return roi.ExistingObjResync.mustResyncTarget(arn) +} + // resyncBucket resyncs all qualifying objects as per replication rules for the target // ARN func (s *replicationResyncer) resyncBucket(ctx context.Context, objectAPI ObjectLayer, heal bool, opts resyncOpts) { @@ -2883,7 +3163,18 @@ func (s *replicationResyncer) resyncBucket(ctx context.Context, objectAPI Object } resyncStatus := ResyncFailed + // workerAborted records that a worker dropped an already-computed result on + // the resync-cancel signal. With a canceled parent context (which makes + // workers return without sending their result), it means a Completed run did + // not actually observe every object - see finalResyncStatus below. + var workerAborted atomic.Bool defer func() { + // Downgrade a Completed status whose counts are incomplete, so the + // persisted status is not a misleading Completed. Runs after results.finish + // drains (LIFO) and before markStatus persists - markStatus uses its own + // background context, so a parent cancellation during the drain would + // otherwise still record Completed. + resyncStatus = finalResyncStatus(resyncStatus, ctx.Err(), workerAborted.Load()) s.markStatus(resyncStatus, opts, objectAPI) globalSiteResyncMetrics.incBucket(opts, resyncStatus) s.workerCh <- struct{}{} @@ -2939,16 +3230,14 @@ func (s *replicationResyncer) resyncBucket(ctx context.Context, objectAPI Object lastCheckpoint = st.Object } workers := make([]chan ReplicateObjectInfo, resyncParallelRoutines) - resultCh := make(chan TargetReplicationResyncStatus, 1) - defer xioutil.SafeClose(resultCh) - go func() { - for r := range resultCh { - s.incStats(r, opts) - globalSiteResyncMetrics.updateMetric(r, opts.resyncID) - } - }() - var wg sync.WaitGroup + // results consumes each worker's per-object outcome and folds it into the + // in-memory status. finish() (deferred below) stops the workers and drains + // every result before the deferred markStatus persists, so a Completed status + // cannot race the last incStats. Registered after the markStatus finalizer, so + // LIFO runs finish first. + results := s.newResyncResults(opts) + defer results.finish(workers, &wg) for i := range resyncParallelRoutines { wg.Add(1) workers[i] = make(chan ReplicateObjectInfo, 100) @@ -2963,6 +3252,7 @@ func (s *replicationResyncer) resyncBucket(ctx context.Context, objectAPI Object default: } traceFn := s.trace(tgt.ResetID, fmt.Sprintf("%s/%s (%s)", opts.bucket, roi.Name, roi.VersionID)) + var rinfos replicatedInfos if roi.DeleteMarker || !roi.VersionPurgeStatus.Empty() { versionID := "" dmVersionID := "" @@ -2985,43 +3275,28 @@ func (s *replicationResyncer) resyncBucket(ctx context.Context, objectAPI Object OpType: replication.ExistingObjectReplicationType, EventType: ReplicateExistingDelete, } - replicateDelete(ctx, doi, objectAPI) + rinfos = replicateDelete(ctx, doi, objectAPI) } else { roi.OpType = replication.ExistingObjectReplicationType roi.EventType = ReplicateExisting - replicateObject(ctx, roi, objectAPI) + rinfos = replicateObject(ctx, roi, objectAPI) } - st := TargetReplicationResyncStatus{ - Object: roi.Name, - Bucket: roi.Bucket, - } - - _, err := tgt.StatObject(ctx, tgt.Bucket, roi.Name, minio.StatObjectOptions{ - VersionID: roi.VersionID, - Internal: minio.AdvancedGetOptions{ - ReplicationProxyRequest: "false", - }, - }) - sz := roi.Size - if err != nil { - if roi.DeleteMarker && isErrMethodNotAllowed(ErrorRespToObjectError(err, opts.bucket, roi.Name)) { - st.ReplicatedCount++ - } else { - st.FailedCount++ + // Classify success/failure from the actual replication outcome + // for this target, not from whether the target version merely + // exists (a rejected update leaves the old version in place). + st := resyncResultFor(rinfos, opts.arn, roi) + var traceSize int64 + var traceErr error + for i := range rinfos.Targets { + if rinfos.Targets[i].Arn == opts.arn { + traceSize, traceErr = rinfos.Targets[i].Size, rinfos.Targets[i].Err + break } - sz = 0 - } else { - st.ReplicatedCount++ - st.ReplicatedSize += roi.Size } - traceFn(sz, err) - select { - case <-ctx.Done(): + traceFn(traceSize, traceErr) + if !s.sendResyncResult(ctx, results.ch, st, &workerAborted) { return - case <-s.resyncCancelCh: - return - case resultCh <- st: } } }(ctx, i) @@ -3045,7 +3320,12 @@ func (s *replicationResyncer) resyncBucket(ctx context.Context, objectAPI Object } lastCheckpoint = "" roi := getHealReplicateObjectInfo(res.Item, rcfg) - if !roi.ExistingObjResync.mustResync() { + // Scope dispatch to this resync's target: the worker pool is for + // opts.arn, so skip objects that only need resync for a different + // target (each target has its own resync). Without this, a cross-target + // object leaves opts.arn absent from its per-object result and is + // miscounted as an opts.arn failure. + if !objectNeedsResyncForARN(roi, opts.arn) { continue } select { @@ -3058,10 +3338,6 @@ func (s *replicationResyncer) resyncBucket(ctx context.Context, objectAPI Object workers[h%uint64(resyncParallelRoutines)] <- roi } } - for i := range resyncParallelRoutines { - xioutil.SafeClose(workers[i]) - } - wg.Wait() resyncStatus = ResyncCompleted } diff --git a/cmd/bucket-replication_test.go b/cmd/bucket-replication_test.go index ada944d20..23a6fd2b7 100644 --- a/cmd/bucket-replication_test.go +++ b/cmd/bucket-replication_test.go @@ -18,12 +18,22 @@ package cmd import ( + "context" + "errors" "fmt" "net/http" + "net/http/httptest" + "path" + "strings" + "sync" + "sync/atomic" "testing" + "testing/synctest" "time" "github.com/minio/madmin-go/v3" + "github.com/minio/minio-go/v7" + objectlock "github.com/minio/minio/internal/bucket/object/lock" "github.com/minio/minio/internal/bucket/replication" xhttp "github.com/minio/minio/internal/http" ) @@ -287,3 +297,808 @@ func TestReplicationResyncwrapper(t *testing.T) { } } } + +func TestReplicationValidationObjectUsesRulePrefix(t *testing.T) { + tests := []struct { + name string + rule replication.Rule + want string + }{ + {name: "empty prefix", rule: replication.Rule{}, want: path.Join(minioReservedBucket, globalLocalNodeNameHex, "deleteme")}, + {name: "filter prefix", rule: replication.Rule{Filter: replication.Filter{Prefix: "data/"}}, want: path.Join("data", minioReservedBucket, globalLocalNodeNameHex, "deleteme")}, + {name: "and prefix", rule: replication.Rule{Filter: replication.Filter{And: replication.And{Prefix: "archive/"}}}, want: path.Join("archive", minioReservedBucket, globalLocalNodeNameHex, "deleteme")}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + if got := replicationValidationObject(test.rule); got != test.want { + t.Fatalf("replicationValidationObject() = %q, want %q", got, test.want) + } + }) + } +} + +// The resync-finalization tests below exercise the real result sink, the +// finish() shutdown ordering, and the sendResyncResult / finalResyncStatus +// helpers, plus (for the persistence cases) markStatus with on-disk +// round-tripping. resyncBucket cannot be driven end to end in a unit test +// because its workers call a live remote target (StatObject), so the helpers it +// uses are exercised directly. The blocking-order assertions run under +// testing/synctest so a removed wait fails deterministically, with no timing +// windows. + +func newTestResyncer(bucket, arn string) (*replicationResyncer, resyncOpts) { + s := &replicationResyncer{ + statusMap: map[string]BucketReplicationResyncStatus{}, + resyncCancelCh: make(chan struct{}, resyncWorkerCnt), + } + brs := newBucketResyncStatus(bucket) + brs.TargetsMap[arn] = TargetReplicationResyncStatus{ResyncStatus: ResyncStarted} + s.statusMap[bucket] = brs + return s, resyncOpts{bucket: bucket, arn: arn, resyncID: "reset-" + bucket} +} + +// TestResyncBucketFinalize round-trips the terminal status through a real +// ObjectLayer: a clean run persists Completed with every result, while a run +// whose parent context was canceled during the drain, or in which a worker +// dropped a result on the cancel signal, is downgraded to Failed so a persisted +// Completed never misrepresents an incomplete resync. +func TestResyncBucketFinalize(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + objAPI, fsDirs, err := prepareErasure16(ctx) + if err != nil { + t.Fatalf("prepare erasure backend: %v", err) + } + defer removeRoots(fsDirs) + + // persistTerminal applies resyncBucket's finalizer logic (finalResyncStatus + // then markStatus, which persists) and reads the status back the way the + // resync status API does. + persistTerminal := func(t *testing.T, s *replicationResyncer, opts resyncOpts, status ResyncStatusType, ctxErr error, aborted bool) TargetReplicationResyncStatus { + t.Helper() + s.markStatus(finalResyncStatus(status, ctxErr, aborted), opts, objAPI) + brs, err := loadBucketResyncMetadata(ctx, opts.bucket, objAPI) + if err != nil { + t.Fatalf("load persisted resync metadata: %v", err) + } + return brs.TargetsMap[opts.arn] + } + + // 1. Clean completion: every result - including the failed object - is folded + // into the persisted status, which stays Completed. + t.Run("persists complete counts", func(t *testing.T) { + s, opts := newTestResyncer("finalize-counts", "arn1") + results := s.newResyncResults(opts) + results.ch <- TargetReplicationResyncStatus{Object: "ok-1", ReplicatedCount: 1, ReplicatedSize: 100} + results.ch <- TargetReplicationResyncStatus{Object: "ok-2", ReplicatedCount: 1, ReplicatedSize: 200} + results.ch <- TargetReplicationResyncStatus{Object: "bad", FailedCount: 1, FailedSize: 300} + + var wg sync.WaitGroup // no producer workers for this case + results.finish(nil, &wg) + + st := persistTerminal(t, s, opts, ResyncCompleted, nil, false) + if st.ResyncStatus != ResyncCompleted { + t.Fatalf("persisted status = %s, want Completed", st.ResyncStatus) + } + if st.ReplicatedCount != 2 || st.ReplicatedSize != 300 || st.FailedCount != 1 || st.FailedSize != 300 { + t.Fatalf("persisted counts = {replicated:%d/%d failed:%d/%d}, want {2/300 1/300}", + st.ReplicatedCount, st.ReplicatedSize, st.FailedCount, st.FailedSize) + } + }) + + // 2. Parent context canceled during the drain -> Completed downgraded to + // Failed (markStatus persists under its own context, so nothing else stops + // a bare Completed from being recorded). + t.Run("parent cancel during drain downgrades to failed", func(t *testing.T) { + s, opts := newTestResyncer("finalize-parent-cancel", "arn1") + results := s.newResyncResults(opts) + results.ch <- TargetReplicationResyncStatus{Object: "ok-1", ReplicatedCount: 1, ReplicatedSize: 100} + var wg sync.WaitGroup + results.finish(nil, &wg) + + cctx, ccancel := context.WithCancel(context.Background()) + ccancel() + st := persistTerminal(t, s, opts, ResyncCompleted, cctx.Err(), false) + if st.ResyncStatus != ResyncFailed { + t.Fatalf("persisted status = %s, want Failed (parent canceled during drain)", st.ResyncStatus) + } + }) + + // 3. A worker dropped a computed result on the resync-cancel token (parent + // still alive) -> sendResyncResult records the abort and Completed is + // downgraded to Failed. + t.Run("worker abort downgrades to failed", func(t *testing.T) { + s, opts := newTestResyncer("finalize-worker-abort", "arn1") + s.resyncCancelCh <- struct{}{} // cancel token waiting + ch := make(chan TargetReplicationResyncStatus) // no reader: the send would block + var aborted atomic.Bool + if s.sendResyncResult(context.Background(), ch, TargetReplicationResyncStatus{Object: "dropped", ReplicatedCount: 1}, &aborted) { + t.Fatal("sendResyncResult reported success despite the cancel token") + } + if !aborted.Load() { + t.Fatal("worker abort was not recorded") + } + st := persistTerminal(t, s, opts, ResyncCompleted, nil, aborted.Load()) + if st.ResyncStatus != ResyncFailed { + t.Fatalf("persisted status = %s, want Failed (worker dropped a result)", st.ResyncStatus) + } + }) +} + +// TestResyncFinishDrainsResults asserts finish() does not return until the +// consumer has applied the final result (the #136 defect). A gated apply holds +// the last result unapplied; under synctest finish() must stay durably blocked +// until it is released - if rr.wg.Wait() is removed, finish() returns early and +// the test fails deterministically. +func TestResyncFinishDrainsResults(t *testing.T) { + synctest.Test(t, func(t *testing.T) { + s, opts := newTestResyncer("drain", "arn1") + reachedFinal := make(chan struct{}) + release := make(chan struct{}) + results := startResyncResults(func(r TargetReplicationResyncStatus) { + if r.Object == "final" { + close(reachedFinal) + <-release + } + s.incStats(r, opts) + }) + results.ch <- TargetReplicationResyncStatus{Object: "ok-1", ReplicatedCount: 1, ReplicatedSize: 100} + results.ch <- TargetReplicationResyncStatus{Object: "final", FailedCount: 1, FailedSize: 200} + <-reachedFinal // consumer received "final" but is gated before incStats(final) + + var wg sync.WaitGroup + finishDone := make(chan struct{}) + go func() { + results.finish(nil, &wg) + close(finishDone) + }() + + synctest.Wait() + select { + case <-finishDone: + close(release) + synctest.Wait() + t.Fatal("finish() returned before the final result was drained (drain wait missing)") + default: + // finish() is durably blocked in rr.wg.Wait() - correct. + } + + close(release) + synctest.Wait() + <-finishDone + st := s.statusMap[opts.bucket].TargetsMap[opts.arn] + if st.ReplicatedCount != 1 || st.FailedCount != 1 || st.FailedSize != 200 { + t.Fatalf("status after finish = {replicated:%d failed:%d/%d}, want {1 1/200}", + st.ReplicatedCount, st.FailedCount, st.FailedSize) + } + }) +} + +// TestResyncFinishWaitsForInflightWorker asserts finish() stops the producer +// workers before it closes the result channel, so an in-flight worker (as on an +// early-return path) never sends on a closed channel and its result is not lost. +// A gated worker stays in flight past the shutdown request; under synctest +// finish() must stay durably blocked until the worker is released - if +// workerWg.Wait() is removed, finish() returns early and the test fails +// deterministically. +func TestResyncFinishWaitsForInflightWorker(t *testing.T) { + synctest.Test(t, func(t *testing.T) { + s, opts := newTestResyncer("workers", "arn1") + results := startResyncResults(func(r TargetReplicationResyncStatus) { s.incStats(r, opts) }) + + workers := []chan ReplicateObjectInfo{make(chan ReplicateObjectInfo, 1)} + var wg sync.WaitGroup + gotRoi := make(chan struct{}) + release := make(chan struct{}) + wg.Add(1) + go func() { + defer wg.Done() + for roi := range workers[0] { + close(gotRoi) + <-release + // Mirror the real worker's send; recover so that if finish() + // wrongly closed the result channel first, the test fails via the + // assertion below instead of crashing on send-on-closed. + func() { + defer func() { _ = recover() }() + results.ch <- TargetReplicationResyncStatus{Object: roi.Name, ReplicatedCount: 1, ReplicatedSize: 500} + }() + } + }() + workers[0] <- ReplicateObjectInfo{Name: "inflight"} + <-gotRoi // worker holds a result in flight, not yet delivered + + finishDone := make(chan struct{}) + go func() { + results.finish(workers, &wg) + close(finishDone) + }() + + synctest.Wait() + select { + case <-finishDone: + close(release) + synctest.Wait() + t.Fatal("finish() closed the result channel before the in-flight worker finished (worker wait missing)") + default: + // finish() is durably blocked in workerWg.Wait() - correct. + } + + close(release) + synctest.Wait() + <-finishDone + st := s.statusMap[opts.bucket].TargetsMap[opts.arn] + if st.ReplicatedCount != 1 || st.ReplicatedSize != 500 { + t.Fatalf("status after finish = {replicated:%d/%d}, want {1/500}", st.ReplicatedCount, st.ReplicatedSize) + } + }) +} + +// TestResyncResultFor asserts the resync worker classifies a target from the +// actual replication outcome, not from whether the target version merely exists. +// The key regression is the "failed update over an existing version" case: a +// quota-rejected update leaves the old version in place, and counting existence +// (the previous behavior) would score it a success. It also checks a genuine +// success, an errored-but-Completed result, a delete failure, a delete-marker +// success (zero bytes), and an ARN that was never attempted. +func TestResyncResultFor(t *testing.T) { + const arn = "arn:minio:replication::id:bucket" + obj := ReplicateObjectInfo{Name: "obj", Bucket: "bucket", Size: 196608} + deleteMarker := ReplicateObjectInfo{Name: "dm", Bucket: "bucket", Size: 0, DeleteMarker: true} + + tests := []struct { + name string + roi ReplicateObjectInfo + rinfos replicatedInfos + wantRepl, wantReplSize, wantFail, wantFailSize int64 + }{ + { + name: "completed update", + roi: obj, + rinfos: replicatedInfos{Targets: []replicatedTargetInfo{ + {Arn: arn, ReplicationStatus: replication.Completed, Size: 196608}, + }}, + wantRepl: 1, wantReplSize: 196608, + }, + { + name: "failed update over existing version", + roi: obj, + rinfos: replicatedInfos{Targets: []replicatedTargetInfo{ + {Arn: arn, ReplicationStatus: replication.Failed, Err: fmt.Errorf("quota exceeded"), Size: 196608}, + }}, + wantFail: 1, wantFailSize: 196608, + }, + { + name: "completed but errored is a failure", + roi: obj, + rinfos: replicatedInfos{Targets: []replicatedTargetInfo{ + {Arn: arn, ReplicationStatus: replication.Completed, Err: fmt.Errorf("boom"), Size: 196608}, + }}, + wantFail: 1, wantFailSize: 196608, + }, + { + name: "delete failed", + roi: deleteMarker, + rinfos: replicatedInfos{Targets: []replicatedTargetInfo{ + {Arn: arn, ReplicationStatus: replication.Failed}, + }}, + wantFail: 1, wantFailSize: 0, + }, + { + name: "delete marker replicated counts zero bytes", + roi: deleteMarker, + rinfos: replicatedInfos{Targets: []replicatedTargetInfo{ + {Arn: arn, ReplicationStatus: replication.Completed}, + }}, + wantRepl: 1, wantReplSize: 0, + }, + { + name: "arn not attempted is a failure", + roi: obj, + rinfos: replicatedInfos{Targets: []replicatedTargetInfo{ + {Arn: "arn:minio:replication::id2:bucket", ReplicationStatus: replication.Completed, Size: 196608}, + }}, + wantFail: 1, wantFailSize: 196608, + }, + { + name: "completed with zero size falls back to object size", + roi: obj, + rinfos: replicatedInfos{Targets: []replicatedTargetInfo{ + {Arn: arn, ReplicationStatus: replication.Completed, Size: 0}, + }}, + wantRepl: 1, wantReplSize: 196608, + }, + { + name: "version purge complete is a success", + roi: ReplicateObjectInfo{Name: "purge", Bucket: "bucket", Size: 196608, VersionPurgeStatus: replication.VersionPurgePending}, + rinfos: replicatedInfos{Targets: []replicatedTargetInfo{ + // a successful purge sets only VersionPurgeStatus; ReplicationStatus stays empty. + {Arn: arn, VersionPurgeStatus: replication.VersionPurgeComplete}, + }}, + wantRepl: 1, wantReplSize: 196608, + }, + { + name: "version purge failed is a failure", + roi: ReplicateObjectInfo{Name: "purge", Bucket: "bucket", Size: 196608, VersionPurgeStatus: replication.VersionPurgePending}, + rinfos: replicatedInfos{Targets: []replicatedTargetInfo{ + {Arn: arn, VersionPurgeStatus: replication.VersionPurgeFailed, Err: fmt.Errorf("quota exceeded")}, + }}, + wantFail: 1, wantFailSize: 196608, + }, + { + name: "benign duplicate 412 is a success", + roi: obj, + rinfos: replicatedInfos{Targets: []replicatedTargetInfo{ + // the destination answers PreconditionFailed for an exact duplicate; + // replicateAll keeps Completed but retains the error. + {Arn: arn, ReplicationStatus: replication.Completed, Err: minio.ErrorResponse{Code: "PreconditionFailed"}, Size: 196608}, + }}, + wantRepl: 1, wantReplSize: 196608, + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + st := resyncResultFor(tc.rinfos, arn, tc.roi) + if st.Object != tc.roi.Name || st.Bucket != tc.roi.Bucket { + t.Fatalf("object/bucket = %s/%s, want %s/%s", st.Object, st.Bucket, tc.roi.Name, tc.roi.Bucket) + } + if st.ReplicatedCount != tc.wantRepl || st.ReplicatedSize != tc.wantReplSize || + st.FailedCount != tc.wantFail || st.FailedSize != tc.wantFailSize { + t.Fatalf("resyncResultFor = {replicated:%d/%d failed:%d/%d}, want {%d/%d %d/%d}", + st.ReplicatedCount, st.ReplicatedSize, st.FailedCount, st.FailedSize, + tc.wantRepl, tc.wantReplSize, tc.wantFail, tc.wantFailSize) + } + }) + } +} + +// TestObjectNeedsResyncForARN asserts the resync dispatch is scoped to the +// target being resynced. The worker pool runs for a single target (opts.arn), +// so an object that only qualifies for a different target must be skipped: with +// A/B rules and a resync of A, an object that needs replication only for B must +// not be admitted to A's worker. Otherwise (after outcome-based classification) +// A would be absent from that object's result and miscounted as an A failure. +func TestObjectNeedsResyncForARN(t *testing.T) { + const ( + arnA = "arn:minio:replication::id:bucket" + arnB = "arn:minio:replication::id2:bucket" + ) + tests := []struct { + name string + decision ResyncDecision + arn string + want bool + }{ + { + name: "target must resync", + decision: ResyncDecision{targets: map[string]ResyncTargetDecision{arnA: {Replicate: true}}}, + arn: arnA, + want: true, + }, + { + name: "object qualifies for B only, resyncing A", + decision: ResyncDecision{targets: map[string]ResyncTargetDecision{arnB: {Replicate: true}}}, + arn: arnA, + want: false, + }, + { + name: "A present but not replicating, B replicating, resyncing A", + decision: ResyncDecision{targets: map[string]ResyncTargetDecision{ + arnA: {Replicate: false}, + arnB: {Replicate: true}, + }}, + arn: arnA, + want: false, + }, + { + name: "object qualifies for both, resyncing A", + decision: ResyncDecision{targets: map[string]ResyncTargetDecision{ + arnA: {Replicate: true}, + arnB: {Replicate: true}, + }}, + arn: arnA, + want: true, + }, + { + name: "no resync decision", + decision: ResyncDecision{}, + arn: arnA, + want: false, + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + roi := ReplicateObjectInfo{Name: "obj", Bucket: "bucket", ExistingObjResync: tc.decision} + if got := objectNeedsResyncForARN(roi, tc.arn); got != tc.want { + t.Fatalf("objectNeedsResyncForARN(arn=%s) = %v, want %v", tc.arn, got, tc.want) + } + }) + } +} + +// newMatchingReplicationPair returns a source/target pair that getReplicationAction must +// classify as replicateNone: same ETag, version id, size, modification time and content +// type. Any action other than replicateNone is therefore attributable to the object lock +// entries a caller adds on top. +func newMatchingReplicationPair() (ObjectInfo, minio.ObjectInfo) { + mtime := time.Date(2026, 9, 5, 10, 0, 0, 0, time.UTC) + size := int64(7) + src := ObjectInfo{ + Bucket: "bucket", + Name: "object", + ETag: "d41d8cd98f00b204e9800998ecf8427e", + VersionID: "b0ff1d6e-0000-4000-8000-000000000001", + Size: size, + ActualSize: &size, + ModTime: mtime, + ContentType: "application/octet-stream", + UserDefined: map[string]string{"content-type": "application/octet-stream"}, + } + tgt := minio.ObjectInfo{ + ETag: src.ETag, + VersionID: src.VersionID, + Size: size, + LastModified: mtime, + ContentType: src.ContentType, + Metadata: http.Header{}, + } + return src, tgt +} + +// TestGetReplicationActionEmptyObjectLockValues covers the comparison of object lock entries +// whose value is empty. Removing retention from a version stores the mode and retain-until-date +// keys with empty values, while the target's HEAD response omits them entirely, so the two must +// compare equal or the version can never be reported as in sync. Cases 3 and 4 are synthetic +// comparison inputs, since a SILO target cannot return empty lock headers; cases 7 and 8 guard +// against over-normalizing. +func TestGetReplicationActionEmptyObjectLockValues(t *testing.T) { + var ( + modeKey = strings.ToLower(xhttp.AmzObjectLockMode) + dateKey = strings.ToLower(xhttp.AmzObjectLockRetainUntilDate) + until = "2026-10-05T10:00:00.000Z" + ) + emptyRetention := map[string]string{modeKey: "", dateKey: ""} + realRetention := map[string]string{modeKey: "GOVERNANCE", dateKey: until} + + tests := []struct { + name string + srcMeta map[string]string + tgtHdr map[string]string + want replicationAction + }{ + {"1-both-clean-never-had-retention", nil, nil, replicateNone}, + {"2-source-present-empty-target-absent", emptyRetention, nil, replicateNone}, + {"3-source-absent-target-present-empty", nil, emptyRetention, replicateNone}, + {"4-both-present-empty", emptyRetention, emptyRetention, replicateNone}, + {"5-both-governance-equal", realRetention, realRetention, replicateNone}, + {"6-source-governance-target-absent", realRetention, nil, replicateMetadata}, + {"7-source-empty-target-real-retention", emptyRetention, realRetention, replicateMetadata}, + {"8-empty-user-metadata-is-not-normalized", map[string]string{"x-amz-meta-foo": ""}, nil, replicateMetadata}, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + src, tgt := newMatchingReplicationPair() + for k, v := range test.srcMeta { + src.UserDefined[k] = v + } + for k, v := range test.tgtHdr { + tgt.Metadata.Set(k, v) + } + if got := getReplicationAction(src, tgt, replication.HealReplicationType); got != test.want { + t.Fatalf("getReplicationAction() = %q, want %q (source %v, target %v)", got, test.want, src.UserDefined, tgt.Metadata) + } + }) + } +} + +// TestEmptyRetentionValuesAreOmittedFromObjectResponseHeaders records why the target half of the +// comparison in getReplicationAction can never report an empty object lock entry: +// FilterObjectLockMetadata drops both keys because an empty mode is not a valid retention mode, +// and setObjectHeaders skips them when writing response headers. Neither filter reaches the +// replication wire: the empty entries are still carried by getCopyObjMetadata and sent by the +// metadata CopyObject, which is why the sender's comparison is what has to tolerate them. +// FilterObjectLockMetadata is also applied by CopyObject (cmd/object-handlers.go:1708), where it +// strips the source's lock metadata before the destination re-derives it from the request. +func TestEmptyRetentionValuesAreOmittedFromObjectResponseHeaders(t *testing.T) { + modeKey := strings.ToLower(xhttp.AmzObjectLockMode) + dateKey := strings.ToLower(xhttp.AmzObjectLockRetainUntilDate) + meta := map[string]string{ + modeKey: "", + dateKey: "", + "content-type": "application/octet-stream", + } + + filtered := objectlock.FilterObjectLockMetadata(meta, false, false) + if _, ok := filtered[modeKey]; ok { + t.Errorf("FilterObjectLockMetadata() kept the empty lock mode key: %v", filtered) + } + if _, ok := filtered[dateKey]; ok { + t.Errorf("FilterObjectLockMetadata() kept the empty retain-until-date key: %v", filtered) + } + + rec := httptest.NewRecorder() + if err := setObjectHeaders(t.Context(), rec, ObjectInfo{UserDefined: meta, ModTime: time.Now(), Size: 7}, nil, ObjectOptions{}); err != nil { + t.Fatalf("setObjectHeaders() = %v", err) + } + if v, ok := rec.Header()[http.CanonicalHeaderKey(xhttp.AmzObjectLockMode)]; ok { + t.Errorf("setObjectHeaders() emitted an empty lock mode header: %v", v) + } + if v, ok := rec.Header()[http.CanonicalHeaderKey(xhttp.AmzObjectLockRetainUntilDate)]; ok { + t.Errorf("setObjectHeaders() emitted an empty retain-until-date header: %v", v) + } +} + +// fakeRetentionGetter answers GetObjectRetention with a fixed result and counts its calls. +type fakeRetentionGetter struct { + mode *minio.RetentionMode + err error + calls int +} + +func (f *fakeRetentionGetter) GetObjectRetention(_ context.Context, _, _, _ string) (*minio.RetentionMode, *time.Time, error) { + f.calls++ + return f.mode, nil, f.err +} + +func TestRetentionRemovedAtSource(t *testing.T) { + modeKey := strings.ToLower(xhttp.AmzObjectLockMode) + dateKey := strings.ToLower(xhttp.AmzObjectLockRetainUntilDate) + tsKey := ReservedMetadataPrefixLower + ObjectLockRetentionTimestamp + stamp := "2026-09-06T01:00:00Z" + tests := []struct { + name string + meta map[string]string + want bool + }{ + {"no lock keys", map[string]string{"content-type": "text/plain"}, false}, + {"empty pair", map[string]string{modeKey: "", dateKey: ""}, true}, + {"empty mode only", map[string]string{modeKey: ""}, true}, + {"empty date only", map[string]string{dateKey: ""}, true}, + {"real retention", map[string]string{modeKey: "GOVERNANCE", dateKey: "2026-10-05T10:00:00.000Z"}, false}, + {"canonical case", map[string]string{xhttp.AmzObjectLockMode: ""}, true}, + {"empty user metadata", map[string]string{"x-amz-meta-foo": ""}, false}, + // Representation (2): a replicated removal persists the ordering timestamp alone, + // with the mode and retain-until-date keys absent (restoreRetention). + {"timestamp only, mode absent", map[string]string{tsKey: stamp}, true}, + {"timestamp with empty mode", map[string]string{tsKey: stamp, modeKey: ""}, true}, + {"timestamp with real retention is a set, not a removal", map[string]string{tsKey: stamp, modeKey: "GOVERNANCE", dateKey: "2026-10-05T10:00:00.000Z"}, false}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + if got := retentionRemovedAtSource(ObjectInfo{UserDefined: test.meta}); got != test.want { + t.Fatalf("retentionRemovedAtSource() = %v, want %v", got, test.want) + } + }) + } +} + +// TestTargetRetentionConfirmedAbsent pins the rule that only an explicit answer from the +// destination clears a removed retention. A denied or unreachable destination must read as still +// holding retention, because HEAD hides a real retention from a credential without +// s3:GetObjectRetention exactly as it hides one that does not exist. +func TestTargetRetentionConfirmedAbsent(t *testing.T) { + governance := minio.Governance + var emptyMode minio.RetentionMode + unknownMode := minio.RetentionMode("ARCHIVE") + tests := []struct { + name string + mode *minio.RetentionMode + err error + want bool + }{ + {"version holds governance retention", &governance, nil, false}, + {"no retention on the version", nil, minio.ErrorResponse{Code: "NoSuchObjectLockConfiguration"}, true}, + { + // The destination also answers this when its own read of the bucket's Object Lock + // configuration fails, so it does not establish that Object Lock is disabled. + "invalid request naming a missing object lock configuration", + nil, + minio.ErrorResponse{Code: "InvalidRequest", Message: "Bucket is missing ObjectLockConfiguration"}, + false, + }, + { + "unrelated invalid request", + nil, + minio.ErrorResponse{Code: "InvalidRequest", Message: "Object is WORM protected and cannot be overwritten"}, + false, + }, + {"retention read denied", nil, minio.ErrorResponse{Code: "AccessDenied"}, false}, + {"destination unreachable", nil, errors.New("dial tcp: connection refused"), false}, + {"empty mode returned", &emptyMode, nil, true}, + {"unknown non-empty mode returned", &unknownMode, nil, false}, + {"nil mode returned", nil, nil, true}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + tgt := &fakeRetentionGetter{mode: test.mode, err: test.err} + if got := targetRetentionConfirmedAbsent(t.Context(), tgt, "bucket", "object", "v1"); got != test.want { + t.Fatalf("targetRetentionConfirmedAbsent() = %v, want %v", got, test.want) + } + if tgt.calls != 1 { + t.Fatalf("GetObjectRetention called %d times, want 1", tgt.calls) + } + }) + } +} + +// TestReplicationActionForTargetRetentionRemoval covers the decision the replication worker makes +// for a version whose retention was removed. The destination's HEAD never reports the empty keys, +// so the comparison alone reads every one of these as in sync; only the confirmation separates a +// destination that really dropped the retention from one that is hiding it. +func TestReplicationActionForTargetRetentionRemoval(t *testing.T) { + modeKey := strings.ToLower(xhttp.AmzObjectLockMode) + dateKey := strings.ToLower(xhttp.AmzObjectLockRetainUntilDate) + governance := minio.Governance + + tests := []struct { + name string + srcMeta map[string]string + mode *minio.RetentionMode + err error + want replicationAction + wantCalls int + }{ + { + name: "removal confirmed by destination", + srcMeta: map[string]string{modeKey: "", dateKey: ""}, + err: minio.ErrorResponse{Code: "NoSuchObjectLockConfiguration"}, + want: replicateNone, + wantCalls: 1, + }, + { + name: "destination still holds the retention hidden from HEAD", + srcMeta: map[string]string{modeKey: "", dateKey: ""}, + mode: &governance, + want: replicateMetadata, + wantCalls: 1, + }, + { + name: "retention hidden from HEAD by permissions", + srcMeta: map[string]string{modeKey: "", dateKey: ""}, + err: minio.ErrorResponse{Code: "AccessDenied"}, + want: replicateMetadata, + wantCalls: 1, + }, + { + // A destination that names a missing Object Lock configuration answers the same way + // when its own read of that configuration failed, so it confirms nothing. + name: "destination reports no object lock configuration", + srcMeta: map[string]string{modeKey: "", dateKey: ""}, + err: minio.ErrorResponse{Code: "InvalidRequest", Message: "Bucket is missing ObjectLockConfiguration"}, + want: replicateMetadata, + wantCalls: 1, + }, + { + name: "version never had retention is not confirmed", + srcMeta: nil, + want: replicateNone, + wantCalls: 0, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + src, tgtInfo := newMatchingReplicationPair() + for k, v := range test.srcMeta { + src.UserDefined[k] = v + } + tgt := &fakeRetentionGetter{mode: test.mode, err: test.err} + got := replicationActionForTarget(t.Context(), src, tgtInfo, replication.HealReplicationType, tgt, "bucket", "object") + if got != test.want { + t.Fatalf("replicationActionForTarget() = %q, want %q", got, test.want) + } + if tgt.calls != test.wantCalls { + t.Fatalf("GetObjectRetention called %d times, want %d", tgt.calls, test.wantCalls) + } + }) + } +} + +// TestReplicationActionForTargetNullVersionResync pins that the confirmation does not reopen the +// null-version exclusion at the head of getReplicationAction. An existing object resync returns +// replicateNone for a null version whose source modification time is later than the target's, +// before comparing anything, and that must stand even when the source carries a removed retention +// and the destination would report retention or refuse to answer. +func TestReplicationActionForTargetNullVersionResync(t *testing.T) { + modeKey := strings.ToLower(xhttp.AmzObjectLockMode) + dateKey := strings.ToLower(xhttp.AmzObjectLockRetainUntilDate) + governance := minio.Governance + + tests := []struct { + name string + mode *minio.RetentionMode + err error + }{ + {"destination holds retention", &governance, nil}, + {"retention read denied", nil, minio.ErrorResponse{Code: "AccessDenied"}}, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + src, tgtInfo := newMatchingReplicationPair() + // A null version whose source modification time is later, and whose content differs, + // so only the exclusion can hold the action at replicateNone. + src.VersionID = nullVersionID + src.ModTime = tgtInfo.LastModified.Add(time.Hour) + src.ETag = "5d41402abc4b2a76b9719d911017c592" + src.UserDefined[modeKey] = "" + src.UserDefined[dateKey] = "" + tgtInfo.VersionID = nullVersionID + + tgt := &fakeRetentionGetter{mode: test.mode, err: test.err} + got := replicationActionForTarget(t.Context(), src, tgtInfo, replication.ExistingObjectReplicationType, tgt, "bucket", "object") + if got != replicateNone { + t.Fatalf("replicationActionForTarget() = %q, want %q", got, replicateNone) + } + if tgt.calls != 0 { + t.Fatalf("GetObjectRetention called %d times, want 0", tgt.calls) + } + }) + } +} + +// TestReplicationActionForTargetTimestampOnlyRemoval covers representation (2) of a removed +// retention. A removal that arrived by replication persists only the retention ordering timestamp, +// with the mode and retain-until-date keys absent, because restoreRetention writes the timestamp +// alone when the mode is empty (cmd/bucket-object-lock.go). The comparison in getReplicationAction +// reads such a source as in sync with a matching destination, so only the GetObjectRetention +// confirmation separates a destination that dropped the retention from one hiding it behind a +// permission-filtered HEAD. The source is built through the real restoreRetention path so the +// fixture is the metadata a replicated removal actually leaves on disk, not a hand-rolled map. +func TestReplicationActionForTargetTimestampOnlyRemoval(t *testing.T) { + governance := minio.Governance + stamp := time.Date(2026, 9, 6, 1, 0, 0, 0, time.UTC).Format(time.RFC3339Nano) + + tests := []struct { + name string + mode *minio.RetentionMode + err error + want replicationAction + wantCalls int + }{ + { + // The reference case: a destination that denies the retention read is + // indistinguishable from one still holding it, so the removal is resent. + name: "retention hidden from HEAD by permissions", + err: minio.ErrorResponse{Code: "AccessDenied"}, + want: replicateMetadata, + wantCalls: 1, + }, + { + name: "destination still holds the retention", + mode: &governance, + want: replicateMetadata, + wantCalls: 1, + }, + { + name: "removal confirmed by destination", + err: minio.ErrorResponse{Code: "NoSuchObjectLockConfiguration"}, + want: replicateNone, + wantCalls: 1, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + src, tgtInfo := newMatchingReplicationPair() + // Persist the timestamp-only tombstone the same way an applied replica removal does. + objectLockState{retentionTimestamp: stamp}.restoreRetention(src.UserDefined) + if !retentionRemovedAtSource(src) { + t.Fatalf("restoreRetention fixture not recognized as a removal: %v", src.UserDefined) + } + if _, ok := src.UserDefined[strings.ToLower(xhttp.AmzObjectLockMode)]; ok { + t.Fatalf("restoreRetention fixture wrote a mode key, fixture is not timestamp-only: %v", src.UserDefined) + } + + tgt := &fakeRetentionGetter{mode: test.mode, err: test.err} + got := replicationActionForTarget(t.Context(), src, tgtInfo, replication.HealReplicationType, tgt, "bucket", "object") + if got != test.want { + t.Fatalf("replicationActionForTarget() = %q, want %q", got, test.want) + } + if tgt.calls != test.wantCalls { + t.Fatalf("GetObjectRetention called %d times, want %d", tgt.calls, test.wantCalls) + } + }) + } +} diff --git a/cmd/bucket-resource-boundary_test.go b/cmd/bucket-resource-boundary_test.go index 07f797f3c..e3ac41af9 100644 --- a/cmd/bucket-resource-boundary_test.go +++ b/cmd/bucket-resource-boundary_test.go @@ -31,7 +31,7 @@ import ( "github.com/minio/madmin-go/v3" "github.com/minio/minio-go/v7" "github.com/minio/minio/internal/auth" - "github.com/minio/pkg/v3/policy" + "github.com/pgsty/silo-pkg/v3/policy" ) // These tests pin the IAM bucket/object resource boundary end to end, through diff --git a/cmd/bucket-versioning-handler.go b/cmd/bucket-versioning-handler.go index 92b2c1466..bd09c2367 100644 --- a/cmd/bucket-versioning-handler.go +++ b/cmd/bucket-versioning-handler.go @@ -28,7 +28,7 @@ import ( "github.com/minio/minio/internal/bucket/versioning" "github.com/minio/minio/internal/logger" "github.com/minio/mux" - "github.com/minio/pkg/v3/policy" + "github.com/pgsty/silo-pkg/v3/policy" ) const ( diff --git a/cmd/common-main.go b/cmd/common-main.go index 18aad7346..7ffe22202 100644 --- a/cmd/common-main.go +++ b/cmd/common-main.go @@ -36,6 +36,8 @@ import ( "strings" "syscall" "time" + "unicode" + "unicode/utf8" "github.com/dustin/go-humanize" fcolor "github.com/fatih/color" @@ -57,10 +59,10 @@ import ( "github.com/minio/minio/internal/handlers" "github.com/minio/minio/internal/kms" "github.com/minio/minio/internal/logger" - "github.com/minio/pkg/v3/certs" - "github.com/minio/pkg/v3/console" - "github.com/minio/pkg/v3/env" - xnet "github.com/minio/pkg/v3/net" + "github.com/pgsty/silo-pkg/v3/certs" + "github.com/pgsty/silo-pkg/v3/console" + "github.com/pgsty/silo-pkg/v3/env" + xnet "github.com/pgsty/silo-pkg/v3/net" "golang.org/x/term" ) @@ -118,15 +120,40 @@ func init() { const consolePrefix = "CONSOLE_" +// consoleMinIOServerEnv derives the CONSOLE_MINIO_SERVER value the embedded +// Console uses to reach the S3/STS API, and whether TLS verification of that +// endpoint must be skipped. With no explicit endpoint configured the Console +// reaches the API over the loopback address, whose TLS certificate is not +// expected to carry a 127.0.0.1 SAN; because Console verifies outbound TLS by +// default, the loopback origin has to be exempted or embedded login (local and +// LDAP alike) fails at the STS handshake. The exemption is endpoint-scoped in +// Console, so every other HTTPS peer stays verified. An explicitly configured +// endpoint is always reached under its own verified name and is never exempted. +func consoleMinIOServerEnv(endpoint string, isTLS bool, port string) (server string, skipVerify bool) { + if endpoint != "" { + return endpoint, false + } + return fmt.Sprintf("%s://127.0.0.1:%s", getURLScheme(isTLS), port), isTLS +} + func minioConfigToConsoleFeatures() { os.Setenv("CONSOLE_PBKDF_SALT", globalDeploymentID()) os.Setenv("CONSOLE_PBKDF_PASSPHRASE", globalDeploymentID()) - if globalMinioEndpoint != "" { - os.Setenv("CONSOLE_MINIO_SERVER", globalMinioEndpoint) + consoleServer, skipVerify := consoleMinIOServerEnv(globalMinioEndpoint, globalIsTLS, globalMinioPort) + os.Setenv("CONSOLE_MINIO_SERVER", consoleServer) + if skipVerify { + // The embedded Console reaches the loopback S3/STS endpoint above, whose + // certificate is not expected to carry a 127.0.0.1 SAN. Console verifies + // outbound TLS by default (silo-console v2.3.x), so opt into the + // endpoint-scoped compatibility switch to preserve the documented loopback + // bypass; every other HTTPS peer (IdP, Prometheus, webhooks, ...) stays + // verified. initConsoleServer unsets CONSOLE_* before calling this, so the + // switch cannot be supplied by the operator on the embedded path. + os.Setenv("CONSOLE_MINIO_SERVER_TLS_SKIP_VERIFY", "on") } else { - // Explicitly set 127.0.0.1 so Console will automatically bypass TLS verification to the local S3 API. - // This will save users from providing a certificate with IP or FQDN SAN that points to the local host. - os.Setenv("CONSOLE_MINIO_SERVER", fmt.Sprintf("%s://127.0.0.1:%s", getURLScheme(globalIsTLS), globalMinioPort)) + // An explicitly configured endpoint is reached under its own verified name; + // never let a loopback exemption apply to it. + os.Unsetenv("CONSOLE_MINIO_SERVER_TLS_SKIP_VERIFY") } if value := env.Get(config.EnvMinIOLogQueryURL, ""); value != "" { os.Setenv("CONSOLE_LOG_QUERY_URL", value) @@ -230,11 +257,31 @@ func buildOpenIDConsoleConfig() consoleoauth2.OpenIDPCfg { return m } -func initConsoleServer() (*consoleapi.Server, error) { - // unset all console_ environment variables. +// resetConsoleEnvironment preserves the embedded Console's supported resource +// settings verbatim. Server derives all other Console settings itself. +func resetConsoleEnvironment() { for _, cenv := range env.List(consolePrefix) { + switch cenv { + case consoleapi.ConsoleWSMaxConnections, + consoleapi.ConsoleWSMaxConnectionsPerClient, + consoleapi.ConsoleWSMaxAnonymousConnections, + consoleapi.ConsoleWSMaxAnonymousConnectionsPerClient: + continue + } os.Unsetenv(cenv) } +} + +func initConsoleServer() (*consoleapi.Server, error) { + resetConsoleEnvironment() + // Validate explicitly: ConfigureAPI logs errors, but embedded Console logs + // are normally silenced. Return configuration failures to Server startup. + if err := consoleapi.ConfigureEmbeddedSourceIPTrust(); err != nil { + return nil, err + } + if err := consoleapi.ConfigureWebSocketLimits(); err != nil { + return nil, err + } // enable all console environment variables minioConfigToConsoleFeatures() @@ -540,6 +587,30 @@ func (e envKV) String() string { return fmt.Sprintf("%s=%s", e.Key, e.Value) } +func isValidEnvName(name string) bool { + if name == "" || !utf8.ValidString(name) { + return false + } + for _, ch := range name { + if ch == '=' || unicode.IsSpace(ch) || !unicode.IsGraphic(ch) { + return false + } + } + return true +} + +func trimExportPrefix(envEntry string) string { + rest, ok := strings.CutPrefix(envEntry, "export") + if !ok || rest == "" { + return envEntry + } + trimmed := strings.TrimLeftFunc(rest, unicode.IsSpace) + if len(trimmed) == len(rest) { + return envEntry + } + return trimmed +} + func parsEnvEntry(envEntry string) (envKV, error) { envEntry = strings.TrimSpace(envEntry) if envEntry == "" { @@ -554,13 +625,19 @@ func parsEnvEntry(envEntry string) (envKV, error) { Skip: true, }, nil } - envTokens := strings.SplitN(strings.TrimSpace(strings.TrimPrefix(envEntry, "export")), config.EnvSeparator, 2) + envTokens := strings.SplitN(trimExportPrefix(envEntry), config.EnvSeparator, 2) if len(envTokens) != 2 { - return envKV{}, fmt.Errorf("envEntry malformed; %s, expected to be of form 'KEY=value'", envEntry) + return envKV{}, errors.New("missing '='") } - key := envTokens[0] - val := envTokens[1] + key := strings.TrimSpace(envTokens[0]) + val := strings.TrimSpace(envTokens[1]) + if !isValidEnvName(key) { + return envKV{}, fmt.Errorf("invalid environment variable name %q", key) + } + if strings.IndexByte(val, 0) >= 0 { + return envKV{}, errors.New("environment variable value contains NUL") + } // Remove quotes from the value if found if len(val) >= 2 { @@ -587,10 +664,12 @@ func minioEnvironFromFile(envConfigFile string) ([]envKV, error) { defer f.Close() var ekvs []envKV scanner := bufio.NewScanner(f) + lineNo := 0 for scanner.Scan() { + lineNo++ ekv, err := parsEnvEntry(scanner.Text()) if err != nil { - return nil, err + return nil, fmt.Errorf("%s:%d: %w", envConfigFile, lineNo, err) } if ekv.Skip { // Skips empty lines @@ -599,7 +678,7 @@ func minioEnvironFromFile(envConfigFile string) ([]envKV, error) { ekvs = append(ekvs, ekv) } if err = scanner.Err(); err != nil { - return nil, err + return nil, fmt.Errorf("%s: %w", envConfigFile, err) } return ekvs, nil } @@ -666,12 +745,15 @@ func loadEnvVarsFromFiles() { } if env.IsSet(config.EnvConfigEnvFile) { - ekvs, err := minioEnvironFromFile(env.Get(config.EnvConfigEnvFile, "")) + envConfigFile := env.Get(config.EnvConfigEnvFile, "") + ekvs, err := minioEnvironFromFile(envConfigFile) if err != nil && !os.IsNotExist(err) { logger.Fatal(err, "Unable to read the config environment file") } for _, ekv := range ekvs { - os.Setenv(ekv.Key, ekv.Value) + if err := os.Setenv(ekv.Key, ekv.Value); err != nil { + logger.Fatal(err, "Unable to set %s from config environment file %s", ekv.Key, envConfigFile) + } } } } diff --git a/cmd/common-main_test.go b/cmd/common-main_test.go index 9757267d2..624c42c92 100644 --- a/cmd/common-main_test.go +++ b/cmd/common-main_test.go @@ -19,9 +19,15 @@ package cmd import ( "errors" + "fmt" "os" "reflect" + "slices" + "strings" "testing" + + consoleapi "github.com/minio/console/api" + "github.com/minio/minio/internal/config" ) func Test_readFromSecret(t *testing.T) { @@ -181,3 +187,324 @@ MINIO_ROOT_PASSWORD=minio123`, }) } } + +func Test_minioEnvironFromFileWhitespaceAndValidation(t *testing.T) { + testCases := []struct { + name string + content string + want []envKV + errLine int + errContains string + errExcludes string + }{ + { + name: "spaces and tabs around separator", + content: "MINIO_ROOT_USER = minio\nMINIO_ROOT_PASSWORD\t=\tminio123", + want: []envKV{ + {Key: "MINIO_ROOT_USER", Value: "minio"}, + {Key: "MINIO_ROOT_PASSWORD", Value: "minio123"}, + }, + }, + { + name: "export tab and quoted spaces", + content: "export\tMINIO_ROOT_USER = \" minio user \"\nexport MINIO_ROOT_PASSWORD = ' minio secret '", + want: []envKV{ + {Key: "MINIO_ROOT_USER", Value: " minio user "}, + {Key: "MINIO_ROOT_PASSWORD", Value: " minio secret "}, + }, + }, + { + name: "export Unicode whitespace", + content: "export\u00a0MINIO_ROOT_USER=value", + want: []envKV{ + {Key: "MINIO_ROOT_USER", Value: "value"}, + }, + }, + { + name: "export is only a standalone prefix", + content: "export=value\nexportFOO=bar", + want: []envKV{ + {Key: "export", Value: "value"}, + {Key: "exportFOO", Value: "bar"}, + }, + }, + { + name: "unquoted whitespace empty value and additional separators", + content: "UNQUOTED = value \nEMPTY =\nTOKEN = scheme://user:password@example.com?a=b", + want: []envKV{ + {Key: "UNQUOTED", Value: "value"}, + {Key: "EMPTY", Value: ""}, + {Key: "TOKEN", Value: "scheme://user:password@example.com?a=b"}, + }, + }, + { + name: "valid underscore and digits", + content: "_VALID_2=value", + want: []envKV{ + {Key: "_VALID_2", Value: "value"}, + }, + }, + { + name: "named target punctuation and unicode", + content: "MINIO_NOTIFY_WEBHOOK_ENABLE_my-hook=off\n" + + "MINIO_NOTIFY_WEBHOOK_ENABLE_site.eu=off\n" + + "MINIO_NOTIFY_WEBHOOK_ENABLE_team:blue=off\n" + + "MINIO_NOTIFY_WEBHOOK_ENABLE_目标=off", + want: []envKV{ + {Key: "MINIO_NOTIFY_WEBHOOK_ENABLE_my-hook", Value: "off"}, + {Key: "MINIO_NOTIFY_WEBHOOK_ENABLE_site.eu", Value: "off"}, + {Key: "MINIO_NOTIFY_WEBHOOK_ENABLE_team:blue", Value: "off"}, + {Key: "MINIO_NOTIFY_WEBHOOK_ENABLE_目标", Value: "off"}, + }, + }, + { + name: "missing separator redacts the line", + content: "MINIO_ROOT_PASSWORD=valid\nsuper-secret-without-equals", + errLine: 2, + errContains: "missing '='", + errExcludes: "super-secret-without-equals", + }, + { + name: "empty name", + content: "=empty-name-secret", + errLine: 1, + errContains: `invalid environment variable name ""`, + errExcludes: "empty-name-secret", + }, + { + name: "os compatible leading digit and punctuation", + content: "1MINIO_ROOT_USER=digit-leading-secret\n-MINIO-ROOT-USER=hyphen-secret", + want: []envKV{ + {Key: "1MINIO_ROOT_USER", Value: "digit-leading-secret"}, + {Key: "-MINIO-ROOT-USER", Value: "hyphen-secret"}, + }, + }, + { + name: "whitespace in name", + content: "MINIO ROOT USER=whitespace-secret", + errLine: 1, + errContains: `invalid environment variable name "MINIO ROOT USER"`, + errExcludes: "whitespace-secret", + }, + { + name: "NUL in name", + content: "MINIO\x00ROOT=nul-name-secret", + errLine: 1, + errContains: "invalid environment variable name", + errExcludes: "nul-name-secret", + }, + { + name: "format character in name", + content: "MINIO\u200bROOT=format-secret", + errLine: 1, + errContains: "invalid environment variable name", + errExcludes: "format-secret", + }, + { + name: "NUL in value", + content: "MINIO_ROOT_USER=before\x00nul-value-secret", + errLine: 1, + errContains: "environment variable value contains NUL", + errExcludes: "nul-value-secret", + }, + { + name: "diagnostic has file and line but no value", + content: "MINIO_ROOT_USER=valid\nBAD KEY=super-secret-value", + errLine: 2, + errContains: `invalid environment variable name "BAD KEY"`, + errExcludes: "super-secret-value", + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + tmpfile, err := os.CreateTemp(t.TempDir(), "testfile") + if err != nil { + t.Fatal(err) + } + if _, err = tmpfile.WriteString(testCase.content); err != nil { + t.Fatal(err) + } + if err = tmpfile.Close(); err != nil { + t.Fatal(err) + } + + got, err := minioEnvironFromFile(tmpfile.Name()) + if testCase.errContains == "" { + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(got, testCase.want) { + t.Errorf("expected %v, got %v", testCase.want, got) + } + return + } + + if err == nil { + t.Fatal("expected an error") + } + errText := err.Error() + location := fmt.Sprintf("%s:%d:", tmpfile.Name(), testCase.errLine) + if !strings.Contains(errText, location) { + t.Errorf("expected error to contain %q, got %q", location, errText) + } + if !strings.Contains(errText, testCase.errContains) { + t.Errorf("expected error to contain %q, got %q", testCase.errContains, errText) + } + if testCase.errExcludes != "" && strings.Contains(errText, testCase.errExcludes) { + t.Errorf("expected error to redact %q, got %q", testCase.errExcludes, errText) + } + if got != nil { + t.Errorf("expected no entries on parse error, got %v", got) + } + }) + } +} + +func TestConfigEnvFileNamedTargetDiscovery(t *testing.T) { + key := "MINIO_NOTIFY_WEBHOOK_ENABLE_my-hook" + t.Setenv(key, "off") + + targets, err := (config.Config{}).GetAvailableTargets(config.NotifyWebhookSubSys) + if err != nil { + t.Fatal(err) + } + if !slices.Contains(targets, "my-hook") { + t.Fatalf("named target %q not discovered from %s: %v", "my-hook", key, targets) + } +} + +// TestConsoleMinIOServerEnv locks in the loopback TLS exemption that keeps +// embedded Console login working (issue #108) while ensuring an explicitly +// configured endpoint is never silently exempted from TLS verification. +func TestConsoleMinIOServerEnv(t *testing.T) { + tests := []struct { + name string + endpoint string + isTLS bool + port string + wantServer string + wantSkipVerify bool + }{ + { + name: "loopback TLS is exempted so embedded login works", + isTLS: true, + port: "9000", + wantServer: "https://127.0.0.1:9000", + wantSkipVerify: true, + }, + { + name: "loopback plain HTTP needs no exemption", + isTLS: false, + port: "9000", + wantServer: "http://127.0.0.1:9000", + }, + { + name: "explicit https endpoint stays verified", + endpoint: "https://silo.example:9000", + isTLS: true, + port: "9000", + wantServer: "https://silo.example:9000", + }, + { + name: "explicit http endpoint stays verified", + endpoint: "http://silo.example:9000", + isTLS: false, + port: "9000", + wantServer: "http://silo.example:9000", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + server, skipVerify := consoleMinIOServerEnv(tt.endpoint, tt.isTLS, tt.port) + if server != tt.wantServer { + t.Fatalf("server = %q, want %q", server, tt.wantServer) + } + if skipVerify != tt.wantSkipVerify { + t.Fatalf("skipVerify = %v, want %v", skipVerify, tt.wantSkipVerify) + } + }) + } +} + +// The startup path clears process environment, so preserve all existing Console +// variables, including ones unrelated to this test, before exercising it. +func preserveConsoleEnvironment(t *testing.T) { + t.Helper() + for _, entry := range os.Environ() { + if strings.HasPrefix(entry, consolePrefix) { + name, value, _ := strings.Cut(entry, "=") + t.Setenv(name, value) + } + } +} + +func TestResetConsoleEnvironment(t *testing.T) { + preserveConsoleEnvironment(t) + settings := map[string]string{ + consoleapi.ConsoleWSMaxConnections: "2048", + consoleapi.ConsoleWSMaxConnectionsPerClient: "512", + consoleapi.ConsoleWSMaxAnonymousConnections: "128", + consoleapi.ConsoleWSMaxAnonymousConnectionsPerClient: "16", + } + for key, value := range settings { + t.Setenv(key, value) + } + decoys := []string{"CONSOLE_MINIO_SERVER_TLS_SKIP_VERIFY", "CONSOLE_MINIO_SERVER", "CONSOLE_PBKDF_SALT", "CONSOLE_TRUSTED_PROXIES", "CONSOLE_WS_MAX_UNKNOWN"} + for _, key := range decoys { + t.Setenv(key, "operator-value") + } + resetConsoleEnvironment() + for key, want := range settings { + if got, present := os.LookupEnv(key); !present || got != want { + t.Errorf("%s = %q, present = %v; want %q", key, got, present, want) + } + } + for _, key := range decoys { + if _, present := os.LookupEnv(key); present { + t.Errorf("unsupported override %s survived", key) + } + } + for _, raw := range []string{"", " 16 ", "env://missing-limit"} { + t.Setenv(consoleapi.ConsoleWSMaxAnonymousConnectionsPerClient, raw) + resetConsoleEnvironment() + if got, present := os.LookupEnv(consoleapi.ConsoleWSMaxAnonymousConnectionsPerClient); !present || got != raw { + t.Fatalf("raw value %q was changed to %q (present = %v)", raw, got, present) + } + } + os.Unsetenv(consoleapi.ConsoleWSMaxAnonymousConnectionsPerClient) + resetConsoleEnvironment() + if _, present := os.LookupEnv(consoleapi.ConsoleWSMaxAnonymousConnectionsPerClient); present { + t.Fatal("unset setting became present") + } +} + +func TestInitConsoleServerConfigurationErrors(t *testing.T) { + for _, tt := range []struct { + name, proxy, limit, want string + }{ + {"proxy error precedes limit error", "proxy.internal", "bad", "MINIO_API_TRUSTED_PROXIES"}, + {"blank limit", "", "", "CONSOLE_WS_MAX_ANONYMOUS_CONNECTIONS_PER_CLIENT"}, + {"non-integer limit", "", "bad", "CONSOLE_WS_MAX_ANONYMOUS_CONNECTIONS_PER_CLIENT"}, + {"out-of-range limit", "", "0", "CONSOLE_WS_MAX_ANONYMOUS_CONNECTIONS_PER_CLIENT"}, + {"inconsistent limits", "", "256", "must be less than"}, + } { + t.Run(tt.name, func(t *testing.T) { + // Restore the process-wide library configuration after environment cleanup. + t.Cleanup(func() { + _ = consoleapi.ConfigureEmbeddedSourceIPTrust() + _ = consoleapi.ConfigureWebSocketLimits() + }) + preserveConsoleEnvironment(t) + t.Setenv(consoleapi.EnvMinIOTrustedProxies, tt.proxy) + t.Setenv(consoleapi.ConsoleWSMaxConnections, "1024") + t.Setenv(consoleapi.ConsoleWSMaxConnectionsPerClient, "256") + t.Setenv(consoleapi.ConsoleWSMaxAnonymousConnections, "64") + t.Setenv(consoleapi.ConsoleWSMaxAnonymousConnectionsPerClient, tt.limit) + server, err := initConsoleServer() + if err == nil || !strings.Contains(err.Error(), tt.want) || server != nil { + t.Fatalf("initConsoleServer() = %v, %v; want nil server and %q error", server, err, tt.want) + } + }) + } +} diff --git a/cmd/compression-ssec_test.go b/cmd/compression-ssec_test.go new file mode 100644 index 000000000..463d562a9 --- /dev/null +++ b/cmd/compression-ssec_test.go @@ -0,0 +1,738 @@ +// Copyright (c) 2015-2026 MinIO, Inc. +// Copyright (c) 2026 PGSTY +// +// This file is part of MinIO Object Storage stack +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package cmd + +import ( + "archive/tar" + "bytes" + "crypto/md5" + "encoding/base64" + "encoding/xml" + "io" + "net/http" + "net/http/httptest" + "strconv" + "testing" + "time" + + "github.com/klauspost/compress/s2" + "github.com/minio/minio/internal/auth" + "github.com/minio/minio/internal/crypto" + xhttp "github.com/minio/minio/internal/http" + "github.com/minio/minio/internal/kms" +) + +// disableCompression turns the global compression config off and returns a +// restore func. It is the replication destination that applies no transform of +// its own; setCopyChecksumCompression covers the enabled cases. +func disableCompression() func() { + globalCompressConfigMu.Lock() + previous := globalCompressConfig + globalCompressConfig.Enabled = false + globalCompressConfigMu.Unlock() + + return func() { + globalCompressConfigMu.Lock() + globalCompressConfig = previous + globalCompressConfigMu.Unlock() + } +} + +// ssecTestHeaders builds the customer key headers for a key made of the given +// repeated byte. +func ssecTestHeaders(b byte) map[string]string { + key := bytes.Repeat([]byte{b}, 32) + keyMD5 := md5.Sum(key) + return map[string]string{ + xhttp.AmzServerSideEncryptionCustomerAlgorithm: xhttp.AmzEncryptionAES, + xhttp.AmzServerSideEncryptionCustomerKey: base64.StdEncoding.EncodeToString(key), + xhttp.AmzServerSideEncryptionCustomerKeyMD5: base64.StdEncoding.EncodeToString(keyMD5[:]), + } +} + +// assertStoredSSECUncompressed requires the stored object to be SSE-C sealed +// and to carry no compression marker, so that "not compressed" is never +// reported for an object that is not encrypted either. +func assertStoredSSECUncompressed(t *testing.T, obj ObjectLayer, bucketName, object string) ObjectInfo { + t.Helper() + info, err := obj.GetObjectInfo(t.Context(), bucketName, object, ObjectOptions{}) + if err != nil { + t.Fatal(err) + } + if _, sealed := info.UserDefined[crypto.MetaSealedKeySSEC]; !sealed { + t.Fatalf("%s is not SSE-C sealed, the fixture proves nothing (userDefined=%v)", object, info.UserDefined) + } + if marker, compressed := info.UserDefined[ReservedMetadataPrefix+"compression"]; compressed { + t.Errorf("%s was stored as a compressed SSE-C object (compression=%q); such an object cannot be replicated", + object, marker) + } + return info +} + +// assertSSECPlaintext GETs an SSE-C object with its customer key and requires +// the body to equal the plaintext. +func assertSSECPlaintext(t *testing.T, apiRouter http.Handler, credentials auth.Credentials, + bucketName, object string, sseHeaders map[string]string, want []byte, +) { + t.Helper() + req, err := newTestSignedRequestV4(http.MethodGet, getGetObjectURL("", bucketName, object), + 0, nil, credentials.AccessKey, credentials.SecretKey, sseHeaders) + if err != nil { + t.Fatal(err) + } + rec := httptest.NewRecorder() + apiRouter.ServeHTTP(rec, req) + if rec.Code != http.StatusOK { + t.Fatalf("GET %s status %d: %s", object, rec.Code, rec.Body.String()) + } + if bytes.Equal(rec.Body.Bytes(), want) { + return + } + body := rec.Body.Bytes() + head := body + if len(head) > 16 { + head = head[:16] + } + t.Errorf("GET %s returned %d bytes, want the %d byte plaintext; first bytes % x", + object, len(body), len(want), head) + // Name the failure mode: a body that s2-decodes to the plaintext is the raw + // S2 stream of a compressed source shipped without its compression marker. + if decoded, derr := io.ReadAll(s2.NewReader(bytes.NewReader(body))); derr == nil && bytes.Equal(decoded, want) { + t.Errorf("the returned body is the raw S2 stream: s2-decoding it yields the %d byte plaintext", len(want)) + } +} + +// TestAPISSECCompressionReplicaStaysReadable replicates an SSE-C object written +// with compression enabled and allow_encryption=on, and requires the replica to +// read back as the source plaintext. +// +// The source object is read the way the replication worker reads it +// (ReplicationRequest, hence NoDecryption), its wire headers come from the +// production option builder putReplicationOpts, and the replica is written the +// way a destination that applies no transform of its own stores it: compression +// off and no default encryption. +// +// Before the SSE-C compression exclusion the source was stored as +// encrypt(s2(plaintext)) while putReplicationOpts dropped +// X-Minio-Internal-compression, so the replica decrypted to an S2 stream and a +// correct-key GET returned HTTP 200 with the wrong body. +func TestAPISSECCompressionReplicaStaysReadable(t *testing.T) { + defer DetectTestLeak(t)() + ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{ + t: t, + objAPITest: testAPISSECCompressionReplicaStaysReadable, + }) +} + +func testAPISSECCompressionReplicaStaysReadable(obj ObjectLayer, instanceType, bucketName string, + apiRouter http.Handler, credentials auth.Credentials, t *testing.T, +) { + previousTLS := globalIsTLS + globalIsTLS = true + defer func() { globalIsTLS = previousTLS }() + + replicator := newObjectAttributesAuthzUser(t, instanceType, bucketName, `"s3:PutObject","s3:GetObject","s3:ReplicateObject"`) + sseHeaders := ssecTestHeaders(0x42) + // Highly compressible and comfortably above minCompressibleSize (4096). + data := bytes.Repeat([]byte("silo compressed ssec replication payload "), 8192) + + t.Run(instanceType+"/single-put", func(t *testing.T) { + object := "replication/ssec-single.txt" + + // --- Source side: compression ON with allow_encryption ON. --- + restore := setCopyChecksumCompression(true) + srcReq, err := newTestSignedRequestV4(http.MethodPut, getPutObjectURL("", bucketName, object), + int64(len(data)), bytes.NewReader(data), credentials.AccessKey, credentials.SecretKey, sseHeaders) + if err != nil { + restore() + t.Fatal(err) + } + srcRec := httptest.NewRecorder() + apiRouter.ServeHTTP(srcRec, srcReq) + if srcRec.Code != http.StatusOK { + restore() + t.Fatalf("source PUT status %d: %s", srcRec.Code, srcRec.Body.String()) + } + sourceInfo := assertStoredSSECUncompressed(t, obj, bucketName, object) + t.Logf("source: stored size=%d compression=%q plaintext=%d", sourceInfo.Size, + sourceInfo.UserDefined[ReservedMetadataPrefix+"compression"], len(data)) + + // The replication worker's read: raw stored bytes, no decryption. + gr, err := obj.GetObjectNInfo(t.Context(), bucketName, object, nil, http.Header{}, + ObjectOptions{ReplicationRequest: true}) + if err != nil { + restore() + t.Fatal(err) + } + sourceInfo = gr.ObjInfo + raw, err := io.ReadAll(gr) + gr.Close() + if err != nil { + restore() + t.Fatal(err) + } + + replicationOpts, isMP, err := putReplicationOpts(t.Context(), "", sourceInfo) + if err != nil { + restore() + t.Fatalf("putReplicationOpts rejected the source: %v", err) + } + if isMP { + restore() + t.Fatal("single PUT source classified as multipart") + } + headers := map[string]string{} + for name, values := range replicationOpts.Header() { + if len(values) > 0 { + headers[name] = values[0] + } + } + restore() + + // --- Destination side: NO compression, NO default encryption. --- + restoreDst := disableCompression() + defer restoreDst() + + replReq, err := newTestSignedRequestV4(http.MethodPut, getPutObjectURL("", bucketName, object), + int64(len(raw)), bytes.NewReader(raw), replicator.AccessKey, replicator.SecretKey, headers) + if err != nil { + t.Fatal(err) + } + replRec := httptest.NewRecorder() + apiRouter.ServeHTTP(replRec, replReq) + if replRec.Code != http.StatusOK { + t.Fatalf("replica PUT status %d: %s", replRec.Code, replRec.Body.String()) + } + + info, err := obj.GetObjectInfo(t.Context(), bucketName, object, ObjectOptions{}) + if err != nil { + t.Fatal(err) + } + t.Logf("replica: stored size=%d compression=%q actual-size=%q", info.Size, + info.UserDefined[ReservedMetadataPrefix+"compression"], + info.UserDefined[ReservedMetadataPrefix+"actual-size"]) + assertSSECPlaintext(t, apiRouter, credentials, bucketName, object, sseHeaders, data) + }) + + t.Run(instanceType+"/multipart", func(t *testing.T) { + object := "replication/ssec-mpu.txt" + + restore := setCopyChecksumCompression(true) + newReq, err := newTestSignedRequestV4(http.MethodPost, getNewMultipartURL("", bucketName, object), + 0, nil, credentials.AccessKey, credentials.SecretKey, sseHeaders) + if err != nil { + restore() + t.Fatal(err) + } + newRec := httptest.NewRecorder() + apiRouter.ServeHTTP(newRec, newReq) + if newRec.Code != http.StatusOK { + restore() + t.Fatalf("source NewMultipart status %d: %s", newRec.Code, newRec.Body.String()) + } + var sourceInit InitiateMultipartUploadResponse + if err = xmlDecoder(newRec.Body, &sourceInit, int64(newRec.Body.Len())); err != nil { + restore() + t.Fatal(err) + } + partReq, err := newTestSignedRequestV4(http.MethodPut, + getPutObjectPartURL("", bucketName, object, sourceInit.UploadID, "1"), + int64(len(data)), bytes.NewReader(data), credentials.AccessKey, credentials.SecretKey, sseHeaders) + if err != nil { + restore() + t.Fatal(err) + } + partRec := httptest.NewRecorder() + apiRouter.ServeHTTP(partRec, partReq) + if partRec.Code != http.StatusOK { + restore() + t.Fatalf("source PutPart status %d: %s", partRec.Code, partRec.Body.String()) + } + completeBody, err := xml.Marshal(CompleteMultipartUpload{Parts: []CompletePart{ + {PartNumber: 1, ETag: canonicalizeETag(partRec.Header()[xhttp.ETag][0])}, + }}) + if err != nil { + restore() + t.Fatal(err) + } + completeReq, err := newTestSignedRequestV4(http.MethodPost, + getCompleteMultipartUploadURL("", bucketName, object, sourceInit.UploadID), + int64(len(completeBody)), bytes.NewReader(completeBody), credentials.AccessKey, credentials.SecretKey, sseHeaders) + if err != nil { + restore() + t.Fatal(err) + } + completeRec := httptest.NewRecorder() + apiRouter.ServeHTTP(completeRec, completeReq) + if completeRec.Code != http.StatusOK { + restore() + t.Fatalf("source Complete status %d: %s", completeRec.Code, completeRec.Body.String()) + } + assertStoredSSECUncompressed(t, obj, bucketName, object) + + gr, err := obj.GetObjectNInfo(t.Context(), bucketName, object, nil, http.Header{}, + ObjectOptions{ReplicationRequest: true}) + if err != nil { + restore() + t.Fatal(err) + } + sourceInfo := gr.ObjInfo + rawPart, err := io.ReadAll(gr) + gr.Close() + if err != nil { + restore() + t.Fatal(err) + } + actualSize, err := sourceInfo.GetActualSize() + if err != nil { + restore() + t.Fatal(err) + } + t.Logf("source mpu: stored size=%d actual-size=%d rawRead=%d plaintext=%d compression=%q", + sourceInfo.Size, actualSize, len(rawPart), len(data), + sourceInfo.UserDefined[ReservedMetadataPrefix+"compression"]) + + replicationOpts, isMP, err := putReplicationOpts(t.Context(), "", sourceInfo) + if err != nil { + restore() + t.Fatalf("putReplicationOpts rejected the source: %v", err) + } + if !isMP { + restore() + t.Fatal("SSE-C multipart source not recognized as multipart") + } + replicationOpts.Internal.SourceMTime = time.Time{} + headers := map[string]string{} + for name, values := range replicationOpts.Header() { + if len(values) > 0 { + headers[name] = values[0] + } + } + restore() + + // --- Destination: no compression, no default encryption. --- + restoreDst := disableCompression() + defer restoreDst() + + replNewReq, err := newTestSignedRequestV4(http.MethodPost, getNewMultipartURL("", bucketName, object), + 0, nil, replicator.AccessKey, replicator.SecretKey, headers) + if err != nil { + t.Fatal(err) + } + replNewRec := httptest.NewRecorder() + apiRouter.ServeHTTP(replNewRec, replNewReq) + if replNewRec.Code != http.StatusOK { + t.Fatalf("replica NewMultipart status %d: %s", replNewRec.Code, replNewRec.Body.String()) + } + var replicaInit InitiateMultipartUploadResponse + if err = xmlDecoder(replNewRec.Body, &replicaInit, int64(replNewRec.Body.Len())); err != nil { + t.Fatal(err) + } + replPartReq, err := newTestSignedRequestV4(http.MethodPut, + getPutObjectPartURL("", bucketName, object, replicaInit.UploadID, "1"), + int64(len(rawPart)), bytes.NewReader(rawPart), replicator.AccessKey, replicator.SecretKey, + map[string]string{xhttp.MinIOSourceReplicationRequest: "true"}) + if err != nil { + t.Fatal(err) + } + replPartRec := httptest.NewRecorder() + apiRouter.ServeHTTP(replPartRec, replPartReq) + if replPartRec.Code != http.StatusOK { + t.Fatalf("replica PutPart status %d: %s", replPartRec.Code, replPartRec.Body.String()) + } + replCompleteBody, err := xml.Marshal(CompleteMultipartUpload{Parts: []CompletePart{ + {PartNumber: 1, ETag: canonicalizeETag(replPartRec.Header()[xhttp.ETag][0])}, + }}) + if err != nil { + t.Fatal(err) + } + replCompleteReq, err := newTestSignedRequestV4(http.MethodPost, + getCompleteMultipartUploadURL("", bucketName, object, replicaInit.UploadID), + int64(len(replCompleteBody)), bytes.NewReader(replCompleteBody), replicator.AccessKey, replicator.SecretKey, + map[string]string{ + xhttp.MinIOSourceReplicationRequest: "true", + xhttp.MinIOSourceMTime: sourceInfo.ModTime.Format(time.RFC3339Nano), + xhttp.MinIOSourceETag: sourceInfo.ETag, + xhttp.MinIOReplicationActualObjectSize: strconv.FormatInt(actualSize, 10), + }) + if err != nil { + t.Fatal(err) + } + replCompleteRec := httptest.NewRecorder() + apiRouter.ServeHTTP(replCompleteRec, replCompleteReq) + if replCompleteRec.Code != http.StatusOK { + t.Fatalf("replica Complete status %d: %s", replCompleteRec.Code, replCompleteRec.Body.String()) + } + + info, err := obj.GetObjectInfo(t.Context(), bucketName, object, ObjectOptions{}) + if err != nil { + t.Fatal(err) + } + reportedActual, aerr := info.GetActualSize() + t.Logf("replica mpu: stored size=%d compression=%q actual-size=%q GetActualSize=%d(err=%v)", + info.Size, info.UserDefined[ReservedMetadataPrefix+"compression"], + info.UserDefined[ReservedMetadataPrefix+"actual-size"], reportedActual, aerr) + assertSSECPlaintext(t, apiRouter, credentials, bucketName, object, sseHeaders, data) + }) +} + +// TestAPISSECCompressionProducerMatrix pins the scope of the exclusion across +// the PutObject and NewMultipartUpload producers: SSE-C is never compressed, +// while plaintext, SSE-S3 and SSE-KMS keep following allow_encryption. +func TestAPISSECCompressionProducerMatrix(t *testing.T) { + defer DetectTestLeak(t)() + ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{ + t: t, + objAPITest: testAPISSECCompressionProducerMatrix, + }) +} + +func testAPISSECCompressionProducerMatrix(obj ObjectLayer, instanceType, bucketName string, + apiRouter http.Handler, credentials auth.Credentials, t *testing.T, +) { + previousTLS := globalIsTLS + globalIsTLS = true + defer func() { globalIsTLS = previousTLS }() + + ssecHeaders := ssecTestHeaders(0x5a) + sseS3Headers := map[string]string{xhttp.AmzServerSideEncryption: xhttp.AmzEncryptionAES} + sseKMSHeaders := map[string]string{ + xhttp.AmzServerSideEncryption: xhttp.AmzEncryptionKMS, + xhttp.AmzServerSideEncryptionKmsID: "compressed-ssec-producer-matrix", + } + previousKMS := GlobalKMS + GlobalKMS = kms.NewStub("compressed-ssec-producer-matrix") + defer func() { GlobalKMS = previousKMS }() + + big := bytes.Repeat([]byte("silo producer matrix payload "), 8192) + small := bytes.Repeat([]byte("s"), 1024) // below minCompressibleSize + + for _, tc := range []struct { + name string + allowEncrypted bool + headers map[string]string + body []byte + wantCompressed bool + }{ + // SSE-C is excluded from compression in both configurations, because the + // replication wire cannot carry the compression state. + {"ssec+allow_encryption-on+large", true, ssecHeaders, big, false}, + {"ssec+allow_encryption-on+small", true, ssecHeaders, small, false}, + {"ssec+allow_encryption-off+large", false, ssecHeaders, big, false}, + // Plaintext still compresses in both configurations. + {"plain+allow_encryption-on+large", true, nil, big, true}, + {"plain+allow_encryption-off+large", false, nil, big, true}, + // SSE-S3 and SSE-KMS are the reason allow_encryption exists: the server + // owns the key, so the source decompresses before replicating. + {"sse-s3+allow_encryption-on+large", true, sseS3Headers, big, true}, + {"sse-s3+allow_encryption-off+large", false, sseS3Headers, big, false}, + {"sse-kms+allow_encryption-on+large", true, sseKMSHeaders, big, true}, + {"sse-kms+allow_encryption-off+large", false, sseKMSHeaders, big, false}, + } { + t.Run(instanceType+"/put/"+tc.name, func(t *testing.T) { + restore := setCopyChecksumCompression(tc.allowEncrypted) + defer restore() + object := "producer/" + tc.name + ".txt" + req, err := newTestSignedRequestV4(http.MethodPut, getPutObjectURL("", bucketName, object), + int64(len(tc.body)), bytes.NewReader(tc.body), credentials.AccessKey, credentials.SecretKey, tc.headers) + if err != nil { + t.Fatal(err) + } + rec := httptest.NewRecorder() + apiRouter.ServeHTTP(rec, req) + if rec.Code != http.StatusOK { + t.Fatalf("PUT status %d, want %d: %s", rec.Code, http.StatusOK, rec.Body.String()) + } + info, err := obj.GetObjectInfo(t.Context(), bucketName, object, ObjectOptions{}) + if err != nil { + t.Fatal(err) + } + _, compressed := info.UserDefined[ReservedMetadataPrefix+"compression"] + if compressed != tc.wantCompressed { + t.Errorf("compressed=%v, want %v (userDefined=%v)", compressed, tc.wantCompressed, info.UserDefined) + } + }) + } + + // NewMultipartUpload has no size gate, so the exclusion turns on the SSE-C + // and allow_encryption combination alone. + for _, tc := range []struct { + name string + allowEncrypted bool + headers map[string]string + wantCompressed bool + }{ + {"ssec+allow_encryption-on", true, ssecHeaders, false}, + {"ssec+allow_encryption-off", false, ssecHeaders, false}, + {"plain+allow_encryption-on", true, nil, true}, + } { + t.Run(instanceType+"/mpu/"+tc.name, func(t *testing.T) { + restore := setCopyChecksumCompression(tc.allowEncrypted) + defer restore() + object := "producer/mpu-" + tc.name + ".txt" + req, err := newTestSignedRequestV4(http.MethodPost, getNewMultipartURL("", bucketName, object), + 0, nil, credentials.AccessKey, credentials.SecretKey, tc.headers) + if err != nil { + t.Fatal(err) + } + rec := httptest.NewRecorder() + apiRouter.ServeHTTP(rec, req) + if rec.Code != http.StatusOK { + t.Fatalf("NewMultipartUpload status %d, want %d: %s", rec.Code, http.StatusOK, rec.Body.String()) + } + var init InitiateMultipartUploadResponse + if err = xmlDecoder(rec.Body, &init, int64(rec.Body.Len())); err != nil { + t.Fatal(err) + } + mi, err := obj.GetMultipartInfo(t.Context(), bucketName, object, init.UploadID, ObjectOptions{}) + if err != nil { + t.Fatal(err) + } + _, compressed := mi.UserDefined[ReservedMetadataPrefix+"compression"] + if compressed != tc.wantCompressed { + t.Errorf("upload compressed=%v, want %v", compressed, tc.wantCompressed) + } + }) + } +} + +// TestAPISSECCompressionSkippedOnCopyObject covers the third producer: +// CopyObjectHandler decides compression before it encrypts, so a copy with a +// destination customer key and allow_encryption=on used to store a compressed +// SSE-C object from a plaintext source. It also covers the reverse direction, +// where only copy-source customer headers are present and compression must +// still apply. +func TestAPISSECCompressionSkippedOnCopyObject(t *testing.T) { + defer DetectTestLeak(t)() + ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{ + t: t, + objAPITest: testAPISSECCompressionSkippedOnCopyObject, + endpoints: []string{"CopyObject", "PutObject", "GetObject"}, + }) +} + +func testAPISSECCompressionSkippedOnCopyObject(obj ObjectLayer, instanceType, bucketName string, + apiRouter http.Handler, credentials auth.Credentials, t *testing.T, +) { + previousTLS := globalIsTLS + globalIsTLS = true + defer func() { globalIsTLS = previousTLS }() + + ssecHeaders := ssecTestHeaders(0x7c) + data := bytes.Repeat([]byte("copy object compressed ssec payload "), 8192) + + restore := setCopyChecksumCompression(true) + defer restore() + + // An unencrypted source, stored compressed because compression is on. Only + // the copy adds encryption, so only the copy can change the decision. + src := "copysrc/plain.txt" + req, err := newTestSignedRequestV4(http.MethodPut, getPutObjectURL("", bucketName, src), + int64(len(data)), bytes.NewReader(data), credentials.AccessKey, credentials.SecretKey, nil) + if err != nil { + t.Fatal(err) + } + rec := httptest.NewRecorder() + apiRouter.ServeHTTP(rec, req) + if rec.Code != http.StatusOK { + t.Fatalf("source PUT status %d: %s", rec.Code, rec.Body.String()) + } + + dst := "copydst/ssec.txt" + copyHeaders := map[string]string{"X-Amz-Copy-Source": SlashSeparator + bucketName + SlashSeparator + src} + for k, v := range ssecHeaders { + copyHeaders[k] = v + } + copyReq, err := newTestSignedRequestV4(http.MethodPut, getCopyObjectURL("", bucketName, dst), + 0, nil, credentials.AccessKey, credentials.SecretKey, copyHeaders) + if err != nil { + t.Fatal(err) + } + copyRec := httptest.NewRecorder() + apiRouter.ServeHTTP(copyRec, copyReq) + if copyRec.Code != http.StatusOK { + t.Fatalf("CopyObject status %d: %s", copyRec.Code, copyRec.Body.String()) + } + + assertStoredSSECUncompressed(t, obj, bucketName, dst) + assertSSECPlaintext(t, apiRouter, credentials, bucketName, dst, ssecHeaders, data) + + // The plaintext source is untouched by the copy and stays compressed. + srcInfo, err := obj.GetObjectInfo(t.Context(), bucketName, src, ObjectOptions{}) + if err != nil { + t.Fatal(err) + } + if _, compressed := srcInfo.UserDefined[ReservedMetadataPrefix+"compression"]; !compressed { + t.Errorf("the plaintext copy source lost compression (userDefined=%v)", srcInfo.UserDefined) + } + + // The reverse direction: a copy-source customer key is not a destination + // key, so copying the SSE-C object on to a plaintext destination still + // compresses. crypto.SSEC.IsRequested ignores the copy-source headers. + plain := "copydst/decrypted.txt" + decryptHeaders := map[string]string{ + "X-Amz-Copy-Source": SlashSeparator + bucketName + SlashSeparator + dst, + xhttp.AmzServerSideEncryptionCopyCustomerAlgorithm: xhttp.AmzEncryptionAES, + xhttp.AmzServerSideEncryptionCopyCustomerKey: ssecHeaders[xhttp.AmzServerSideEncryptionCustomerKey], + xhttp.AmzServerSideEncryptionCopyCustomerKeyMD5: ssecHeaders[xhttp.AmzServerSideEncryptionCustomerKeyMD5], + } + decryptReq, err := newTestSignedRequestV4(http.MethodPut, getCopyObjectURL("", bucketName, plain), + 0, nil, credentials.AccessKey, credentials.SecretKey, decryptHeaders) + if err != nil { + t.Fatal(err) + } + decryptRec := httptest.NewRecorder() + apiRouter.ServeHTTP(decryptRec, decryptReq) + if decryptRec.Code != http.StatusOK { + t.Fatalf("CopyObject to a plaintext destination status %d: %s", decryptRec.Code, decryptRec.Body.String()) + } + plainInfo, err := obj.GetObjectInfo(t.Context(), bucketName, plain, ObjectOptions{}) + if err != nil { + t.Fatal(err) + } + if _, sealed := plainInfo.UserDefined[crypto.MetaSealedKeySSEC]; sealed { + t.Fatalf("%s is still SSE-C sealed, the fixture proves nothing", plain) + } + if _, compressed := plainInfo.UserDefined[ReservedMetadataPrefix+"compression"]; !compressed { + t.Errorf("a copy carrying only copy-source SSE-C headers was not compressed (userDefined=%v)", plainInfo.UserDefined) + } +} + +// TestAPISSECCompressionSkippedOnSnowballExtract covers the fourth producer: +// PutObjectExtractHandler decides compression per entry before it encrypts, so +// a tar extract carrying customer key headers used to store every entry as a +// compressed SSE-C object. +func TestAPISSECCompressionSkippedOnSnowballExtract(t *testing.T) { + defer DetectTestLeak(t)() + ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{ + t: t, + objAPITest: testAPISSECCompressionSkippedOnSnowballExtract, + }) +} + +func testAPISSECCompressionSkippedOnSnowballExtract(obj ObjectLayer, instanceType, bucketName string, + apiRouter http.Handler, credentials auth.Credentials, t *testing.T, +) { + previousTLS := globalIsTLS + globalIsTLS = true + defer func() { globalIsTLS = previousTLS }() + + entry := "extracted/entry.txt" + payload := bytes.Repeat([]byte("snowball compressed ssec entry "), 4096) + + var body bytes.Buffer + tw := tar.NewWriter(&body) + if err := tw.WriteHeader(&tar.Header{Name: entry, Mode: 0o600, Size: int64(len(payload))}); err != nil { + t.Fatal(err) + } + if _, err := tw.Write(payload); err != nil { + t.Fatal(err) + } + if err := tw.Close(); err != nil { + t.Fatal(err) + } + + restore := setCopyChecksumCompression(true) + defer restore() + + ssecHeaders := ssecTestHeaders(0x2d) + headers := map[string]string{xhttp.AmzSnowballExtract: "true"} + for k, v := range ssecHeaders { + headers[k] = v + } + req, err := newTestSignedRequestV4(http.MethodPut, getPutObjectURL("", bucketName, "snowball.tar"), + int64(body.Len()), bytes.NewReader(body.Bytes()), credentials.AccessKey, credentials.SecretKey, headers) + if err != nil { + t.Fatal(err) + } + rec := httptest.NewRecorder() + apiRouter.ServeHTTP(rec, req) + if rec.Code != http.StatusOK { + t.Fatalf("snowball extract status %d: %s", rec.Code, rec.Body.String()) + } + + assertStoredSSECUncompressed(t, obj, bucketName, entry) + assertSSECPlaintext(t, apiRouter, credentials, bucketName, entry, ssecHeaders, payload) +} + +// TestSSECBatchReplicationCannotRead is the control for the corruption path: +// batch replication reads without ReplicationRequest, so NoDecryption is never +// set and a non-empty SSE-C source fails at read time. Batch replication +// therefore cannot reach the replica shape in +// TestAPISSECCompressionReplicaStaysReadable; it cannot replicate a non-empty +// SSE-C object at all, compressed or not. A zero-byte object takes the reader +// shortcut, whose key check passes without a customer key. +func TestSSECBatchReplicationCannotRead(t *testing.T) { + defer DetectTestLeak(t)() + ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{ + t: t, + objAPITest: testSSECBatchReplicationCannotRead, + }) +} + +func testSSECBatchReplicationCannotRead(obj ObjectLayer, instanceType, bucketName string, + apiRouter http.Handler, credentials auth.Credentials, t *testing.T, +) { + previousTLS := globalIsTLS + globalIsTLS = true + defer func() { globalIsTLS = previousTLS }() + + ssecHeaders := ssecTestHeaders(0x6b) + data := bytes.Repeat([]byte("batch ssec payload "), 8192) + object := "batch/ssec-plain.txt" + + restore := disableCompression() + defer restore() + + req, err := newTestSignedRequestV4(http.MethodPut, getPutObjectURL("", bucketName, object), + int64(len(data)), bytes.NewReader(data), credentials.AccessKey, credentials.SecretKey, ssecHeaders) + if err != nil { + t.Fatal(err) + } + rec := httptest.NewRecorder() + apiRouter.ServeHTTP(rec, req) + if rec.Code != http.StatusOK { + t.Fatalf("source PUT status %d: %s", rec.Code, rec.Body.String()) + } + + // The read shape used by BatchJobReplicateV1.ReplicateToTarget and + // writeAsArchive: no ReplicationRequest, so NoDecryption is never set. + gr, err := obj.GetObjectNInfo(t.Context(), bucketName, object, nil, http.Header{}, ObjectOptions{}) + if err == nil { + gr.Close() + t.Fatal("batch-shaped read of an SSE-C object unexpectedly succeeded") + } + t.Logf("batch-shaped read of an SSE-C object fails as expected: %v", err) + + // Control: the replication worker's read shape succeeds and yields ciphertext. + gr2, err := obj.GetObjectNInfo(t.Context(), bucketName, object, nil, http.Header{}, + ObjectOptions{ReplicationRequest: true}) + if err != nil { + t.Fatalf("replication-shaped read failed: %v", err) + } + raw, err := io.ReadAll(gr2) + gr2.Close() + if err != nil { + t.Fatal(err) + } + if bytes.Equal(raw, data) { + t.Fatal("replication-shaped read returned plaintext") + } + t.Logf("replication-shaped read returns %d bytes of ciphertext (plaintext %d)", len(raw), len(data)) +} diff --git a/cmd/config-current.go b/cmd/config-current.go index 80abfb84b..508df6c96 100644 --- a/cmd/config-current.go +++ b/cmd/config-current.go @@ -52,7 +52,7 @@ import ( "github.com/minio/minio/internal/crypto" xhttp "github.com/minio/minio/internal/http" "github.com/minio/minio/internal/logger" - "github.com/minio/pkg/v3/env" + "github.com/pgsty/silo-pkg/v3/env" ) func initHelp() { diff --git a/cmd/config-migrate.go b/cmd/config-migrate.go index 0cceb1859..aa74a13e1 100644 --- a/cmd/config-migrate.go +++ b/cmd/config-migrate.go @@ -33,8 +33,8 @@ import ( "github.com/minio/minio/internal/config/storageclass" "github.com/minio/minio/internal/event/target" "github.com/minio/minio/internal/logger" - xnet "github.com/minio/pkg/v3/net" - "github.com/minio/pkg/v3/quick" + xnet "github.com/pgsty/silo-pkg/v3/net" + "github.com/pgsty/silo-pkg/v3/quick" ) // Save config file to corresponding backend @@ -167,7 +167,9 @@ func readConfigWithoutMigrate(ctx context.Context, objAPI ObjectLayer) (config.C notify.SetNotifyMQTT(newCfg, k, args) } for k, args := range cfg.Notify.MySQL { - notify.SetNotifyMySQL(newCfg, k, args) + if err := notify.SetNotifyMySQL(newCfg, k, args); err != nil { + return nil, err + } } for k, args := range cfg.Notify.NATS { notify.SetNotifyNATS(newCfg, k, args) @@ -176,7 +178,9 @@ func readConfigWithoutMigrate(ctx context.Context, objAPI ObjectLayer) (config.C notify.SetNotifyNSQ(newCfg, k, args) } for k, args := range cfg.Notify.PostgreSQL { - notify.SetNotifyPostgres(newCfg, k, args) + if err := notify.SetNotifyPostgres(newCfg, k, args); err != nil { + return nil, err + } } for k, args := range cfg.Notify.Redis { notify.SetNotifyRedis(newCfg, k, args) diff --git a/cmd/config-migrate_test.go b/cmd/config-migrate_test.go new file mode 100644 index 000000000..76a616d9b --- /dev/null +++ b/cmd/config-migrate_test.go @@ -0,0 +1,264 @@ +// Copyright (c) 2015-2026 MinIO, Inc. +// +// This file is part of MinIO Object Storage stack +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package cmd + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "os" + "reflect" + "strings" + "testing" + + "github.com/minio/minio/internal/config" + "github.com/minio/minio/internal/config/notify" + "github.com/minio/minio/internal/event/target" +) + +func installLegacyConfigFile(t *testing.T, configure func(*serverConfigV33)) (string, []byte) { + t.Helper() + + cfg := &serverConfigV33{ + Version: "33", + Notify: notify.NewConfig(), + } + configure(cfg) + + data, err := json.Marshal(cfg) + if err != nil { + t.Fatal(err) + } + + oldConfigDir := globalConfigDir + globalConfigDir = &ConfigDir{path: t.TempDir()} + t.Cleanup(func() { globalConfigDir = oldConfigDir }) + + configFile := getConfigFile() + if err = os.WriteFile(configFile, data, 0o600); err != nil { + t.Fatal(err) + } + return configFile, data +} + +func assertLegacyMigrationError(t *testing.T, err error, subsystem, name, key, secret string) { + t.Helper() + var targetErr *notify.LegacyDatabaseTargetError + if !errors.As(err, &targetErr) { + t.Fatalf("error = %v, want *notify.LegacyDatabaseTargetError", err) + } + msg := err.Error() + for _, want := range []string{subsystem + config.SubSystemSeparator + name, key} { + if !strings.Contains(msg, want) { + t.Errorf("error %q does not contain %q", msg, want) + } + } + if strings.Contains(msg, secret) { + t.Errorf("migration error leaks database password %q: %s", secret, msg) + } +} + +func TestReadConfigWithoutMigrateRejectsLegacyDatabaseTargets(t *testing.T) { + tests := []struct { + name string + subsystem string + key string + secret string + configure func(*serverConfigV33) + }{ + { + name: "postgres", + subsystem: config.NotifyPostgresSubSys, + key: target.PostgresConnectionString, + secret: "postgres-migration-secret", + configure: func(cfg *serverConfigV33) { + cfg.Notify.PostgreSQL["archive"] = target.PostgreSQLArgs{ + Enable: true, + Port: "5432", + Username: "legacy-user", + Password: "postgres-migration-secret", + Database: "events", + } + }, + }, + { + name: "mysql", + subsystem: config.NotifyMySQLSubSys, + key: target.MySQLDSNString, + secret: "mysql-migration-secret", + configure: func(cfg *serverConfigV33) { + cfg.Notify.MySQL["archive"] = target.MySQLArgs{ + Enable: true, + Port: "3306", + User: "legacy-user", + Password: "mysql-migration-secret", + Database: "events", + } + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + configFile, original := installLegacyConfigFile(t, test.configure) + got, err := readConfigWithoutMigrate(t.Context(), nil) + if got != nil { + t.Fatalf("config = %v, want nil on failed migration", got) + } + assertLegacyMigrationError(t, err, test.subsystem, "archive", test.key, test.secret) + + after, readErr := os.ReadFile(configFile) + if readErr != nil { + t.Fatal(readErr) + } + if !bytes.Equal(after, original) { + t.Fatal("failed migration rewrote the legacy source config") + } + if _, statErr := os.Stat(configFile + ".old"); !errors.Is(statErr, os.ErrNotExist) { + t.Fatalf("failed migration created a backup/persistence artifact: %v", statErr) + } + }) + } +} + +func TestReadConfigWithoutMigrateMigratesCanonicalDatabaseTargets(t *testing.T) { + const ( + postgresConnection = "host=postgres.example port=5432 dbname=events user=app password=secret sslmode=disable" + mysqlDSN = "app:secret@tcp(mysql.example:3306)/events?parseTime=true" + discardedLegacyValue = "discarded-legacy-value" + ) + installLegacyConfigFile(t, func(cfg *serverConfigV33) { + cfg.Notify.PostgreSQL["archive"] = target.PostgreSQLArgs{ + Enable: true, + Format: "namespace", + ConnectionString: postgresConnection, + Table: "events", + Port: discardedLegacyValue, + Username: discardedLegacyValue, + Password: discardedLegacyValue, + Database: discardedLegacyValue, + } + cfg.Notify.MySQL["archive"] = target.MySQLArgs{ + Enable: true, + Format: "namespace", + DSN: mysqlDSN, + Table: "events", + Port: discardedLegacyValue, + User: discardedLegacyValue, + Password: discardedLegacyValue, + Database: discardedLegacyValue, + } + }) + + got, err := readConfigWithoutMigrate(t.Context(), nil) + if err != nil { + t.Fatalf("readConfigWithoutMigrate: %v", err) + } + tests := []struct { + subsystem string + key string + want string + discarded string + }{ + {config.NotifyPostgresSubSys, target.PostgresConnectionString, postgresConnection, discardedLegacyValue}, + {config.NotifyMySQLSubSys, target.MySQLDSNString, mysqlDSN, discardedLegacyValue}, + } + for _, test := range tests { + kvs := got[test.subsystem]["archive"] + if value := kvs.Get(test.key); value != test.want { + t.Errorf("%s %s = %q, want %q", test.subsystem, test.key, value, test.want) + } + if err := config.CheckValidKeys(test.subsystem+config.SubSystemSeparator+"archive", kvs, notify.DefaultNotificationKVS[test.subsystem]); err != nil { + t.Errorf("migrated %s target failed key validation: %v", test.subsystem, err) + } + for _, key := range []string{"host", "port", "username", "password", "database"} { + if _, ok := kvs.Lookup(key); ok { + t.Errorf("migrated %s target contains legacy key %q", test.subsystem, key) + } + } + for _, kv := range kvs { + if strings.Contains(kv.Value, test.discarded) { + t.Errorf("migrated %s target contains discarded legacy value in %q", test.subsystem, kv.Key) + } + } + } + + postgresTargets, err := notify.GetNotifyPostgres(got[config.NotifyPostgresSubSys]) + if err != nil { + t.Fatalf("GetNotifyPostgres: %v", err) + } + if value := postgresTargets["archive"].ConnectionString; value != postgresConnection { + t.Errorf("Postgres connection string = %q, want %q", value, postgresConnection) + } + mysqlTargets, err := notify.GetNotifyMySQL(got[config.NotifyMySQLSubSys]) + if err != nil { + t.Fatalf("GetNotifyMySQL: %v", err) + } + if value := mysqlTargets["archive"].DSN; value != mysqlDSN { + t.Errorf("MySQL DSN = %q, want %q", value, mysqlDSN) + } +} + +func TestInitConfigSubsystemReturnsLegacyDatabaseTargetError(t *testing.T) { + obj, fsDir, err := prepareFS(t.Context()) + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { + _ = obj.Shutdown(context.Background()) + _ = os.RemoveAll(fsDir) + }) + + const secret = "startup-migration-secret" + installLegacyConfigFile(t, func(cfg *serverConfigV33) { + cfg.Notify.PostgreSQL["archive"] = target.PostgreSQLArgs{ + Enable: true, + Port: "5432", + Username: "legacy-user", + Password: secret, + Database: "events", + } + }) + + globalServerConfigMu.RLock() + var before config.Config + if globalServerConfig != nil { + before = globalServerConfig.Clone() + } + globalServerConfigMu.RUnlock() + + err = initConfigSubsystem(t.Context(), obj) + assertLegacyMigrationError(t, err, config.NotifyPostgresSubSys, "archive", target.PostgresConnectionString, secret) + if configRetriableErrors(err) { + t.Fatal("legacy database migration error must be startup-fatal, not retriable") + } + if !fatalServerConfigError(err) { + t.Fatal("legacy database migration error must abort server startup") + } + + globalServerConfigMu.RLock() + var after config.Config + if globalServerConfig != nil { + after = globalServerConfig.Clone() + } + globalServerConfigMu.RUnlock() + if !reflect.DeepEqual(after, before) { + t.Fatal("failed migration activated a partial server configuration") + } +} diff --git a/cmd/config-versions.go b/cmd/config-versions.go index 020bfa440..2d622c397 100644 --- a/cmd/config-versions.go +++ b/cmd/config-versions.go @@ -27,7 +27,7 @@ import ( "github.com/minio/minio/internal/config/policy/opa" "github.com/minio/minio/internal/config/storageclass" "github.com/minio/minio/internal/logger" - "github.com/minio/pkg/v3/quick" + "github.com/pgsty/silo-pkg/v3/quick" ) // FileLogger is introduced to workaround the dependency about logrus diff --git a/cmd/consolelogger.go b/cmd/consolelogger.go index 18488e192..a6462b2ab 100644 --- a/cmd/consolelogger.go +++ b/cmd/consolelogger.go @@ -30,7 +30,7 @@ import ( "github.com/minio/minio/internal/logger/target/console" types "github.com/minio/minio/internal/logger/target/loggertypes" "github.com/minio/minio/internal/pubsub" - xnet "github.com/minio/pkg/v3/net" + xnet "github.com/pgsty/silo-pkg/v3/net" ) // number of log messages to buffer diff --git a/cmd/data-scanner.go b/cmd/data-scanner.go index e2182e6c4..bb2b3005b 100644 --- a/cmd/data-scanner.go +++ b/cmd/data-scanner.go @@ -42,7 +42,7 @@ import ( "github.com/minio/minio/internal/config/heal" "github.com/minio/minio/internal/event" xioutil "github.com/minio/minio/internal/ioutil" - "github.com/minio/pkg/v3/console" + "github.com/pgsty/silo-pkg/v3/console" uatomic "go.uber.org/atomic" ) diff --git a/cmd/data-usage-cache.go b/cmd/data-usage-cache.go index 8d1e78aba..405d85877 100644 --- a/cmd/data-usage-cache.go +++ b/cmd/data-usage-cache.go @@ -351,7 +351,7 @@ func (h dataUsageHash) modAlt(cycle uint32, cycles uint32) bool { if cycles <= 1 { return cycles == 1 } - return uint32(xxhash.Sum64String(string(h))>>32)%(cycles) == cycle%cycles + return uint32(xxhash.Sum64String(string(h))>>32)%cycles == cycle%cycles } // addChild will add a child based on its hash. diff --git a/cmd/delete-version-authz_test.go b/cmd/delete-version-authz_test.go new file mode 100644 index 000000000..8ce28ea19 --- /dev/null +++ b/cmd/delete-version-authz_test.go @@ -0,0 +1,402 @@ +// Copyright (c) 2015-2026 MinIO, Inc. +// Copyright (c) 2026 PGSTY +// +// This file is part of MinIO Object Storage stack +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +package cmd + +import ( + "bytes" + "encoding/xml" + "net/http" + "net/http/httptest" + "net/url" + "strings" + "testing" + "time" + + "github.com/minio/madmin-go/v3" + "github.com/minio/minio/internal/auth" + xhttp "github.com/minio/minio/internal/http" + "github.com/pgsty/silo-pkg/v3/policy" +) + +func TestDeleteObjectAction(t *testing.T) { + for _, test := range []struct { + versionID string + want policy.Action + }{ + {want: policy.DeleteObjectAction}, + {versionID: nullVersionID, want: policy.DeleteObjectVersionAction}, + {versionID: mustGetUUID(), want: policy.DeleteObjectVersionAction}, + {versionID: " ", want: policy.DeleteObjectVersionAction}, + } { + if got := deleteObjectAction(test.versionID); got != test.want { + t.Errorf("deleteObjectAction(%q) = %s, want %s", test.versionID, got, test.want) + } + } +} + +func TestAPIDeleteObjectVersionAuthorization(t *testing.T) { + defer DetectTestLeak(t)() + ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{ + t: t, + objAPITest: testAPIDeleteObjectVersionAuthorization, + endpoints: []string{"DeleteObject"}, + makeBucketOptions: MakeBucketOptions{VersioningEnabled: true}, + }) +} + +func TestAPIDeleteMultipleObjectsVersionAuthorization(t *testing.T) { + defer DetectTestLeak(t)() + ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{ + t: t, + objAPITest: testAPIDeleteMultipleObjectsVersionAuthorization, + endpoints: []string{"DeleteMultipleObjects"}, + makeBucketOptions: MakeBucketOptions{VersioningEnabled: true}, + }) +} + +func testAPIDeleteMultipleObjectsVersionAuthorization(obj ObjectLayer, instanceType, bucket string, + apiRouter http.Handler, _ auth.Credentials, t *testing.T, +) { + deleteOnly := newObjectAttributesAuthzUser(t, instanceType, bucket, `"s3:DeleteObject"`) + versionOnly := newObjectAttributesAuthzUser(t, instanceType, bucket, `"s3:DeleteObjectVersion"`) + payload := []byte("multi delete version authorization") + + put := func(t *testing.T, object string, versioned bool) string { + t.Helper() + info, err := obj.PutObject(t.Context(), bucket, object, + mustGetPutObjReader(t, bytes.NewReader(payload), int64(len(payload)), "", ""), ObjectOptions{Versioned: versioned}) + if err != nil { + t.Fatal(err) + } + return info.VersionID + } + request := func(t *testing.T, prefix string, creds auth.Credentials) (DeleteObjectsResponse, map[string]string) { + t.Helper() + versions := map[string]string{ + prefix + "simple": put(t, prefix+"simple", true), + prefix + "explicit": put(t, prefix+"explicit", true), + prefix + "null": put(t, prefix+"null", false), + } + body := encodeResponse(DeleteObjectsRequest{Objects: []ObjectToDelete{ + {ObjectV: ObjectV{ObjectName: prefix + "simple"}}, + {ObjectV: ObjectV{ObjectName: prefix + "explicit", VersionID: versions[prefix+"explicit"]}}, + {ObjectV: ObjectV{ObjectName: prefix + "null", VersionID: nullVersionID}}, + {ObjectV: ObjectV{ObjectName: prefix + "bad", VersionID: "not-a-uuid"}}, + }}) + target := getDeleteMultipleObjectsURL("", bucket) + "&versionId=query-level-decoy" + req, err := newTestSignedRequestV4(http.MethodPost, target, int64(len(body)), bytes.NewReader(body), + creds.AccessKey, creds.SecretKey, nil) + if err != nil { + t.Fatal(err) + } + rec := httptest.NewRecorder() + apiRouter.ServeHTTP(rec, req) + if rec.Code != http.StatusOK { + t.Fatalf("%s: multi-delete status %d: %s", instanceType, rec.Code, rec.Body.String()) + } + var response DeleteObjectsResponse + if err = xml.Unmarshal(rec.Body.Bytes(), &response); err != nil { + t.Fatalf("decode response: %v: %s", err, rec.Body.String()) + } + return response, versions + } + responseMap := func(response DeleteObjectsResponse) (map[string]DeletedObject, map[string]DeleteError) { + deleted := make(map[string]DeletedObject, len(response.DeletedObjects)) + for _, object := range response.DeletedObjects { + deleted[object.ObjectName] = object + } + errs := make(map[string]DeleteError, len(response.Errors)) + for _, deleteErr := range response.Errors { + errs[deleteErr.Key] = deleteErr + } + return deleted, errs + } + + t.Run("DeleteObject only", func(t *testing.T) { + prefix := "multi-delete-only/" + response, versions := request(t, prefix, deleteOnly) + deleted, errs := responseMap(response) + if object, ok := deleted[prefix+"simple"]; !ok || !object.DeleteMarker { + t.Fatalf("simple delete did not create a marker: %+v", response) + } + for _, object := range []string{"explicit", "null", "bad"} { + if got := errs[prefix+object].Code; got != errorCodes[ErrAccessDenied].Code { + t.Errorf("%s error = %q, want AccessDenied", object, got) + } + } + if _, err := obj.GetObjectInfo(t.Context(), bucket, prefix+"explicit", ObjectOptions{VersionID: versions[prefix+"explicit"]}); err != nil { + t.Fatalf("denied explicit delete removed its version: %v", err) + } + }) + + t.Run("DeleteObjectVersion only", func(t *testing.T) { + prefix := "multi-version-only/" + response, _ := request(t, prefix, versionOnly) + deleted, errs := responseMap(response) + for _, object := range []string{"explicit", "null"} { + if _, ok := deleted[prefix+object]; !ok { + t.Errorf("%s was not deleted: %+v", object, response) + } + } + if got := errs[prefix+"simple"].Code; got != errorCodes[ErrAccessDenied].Code { + t.Errorf("simple error = %q, want AccessDenied", got) + } + if got := errs[prefix+"bad"].Code; got != errorCodes[ErrNoSuchVersion].Code { + t.Errorf("bad UUID error = %q, want NoSuchVersion", got) + } + }) +} + +func testAPIDeleteObjectVersionAuthorization(obj ObjectLayer, instanceType, bucket string, + apiRouter http.Handler, _ auth.Credentials, t *testing.T, +) { + deleteOnly := newObjectAttributesAuthzUser(t, instanceType, bucket, `"s3:DeleteObject"`) + versionOnly := newObjectAttributesAuthzUser(t, instanceType, bucket, `"s3:DeleteObjectVersion"`) + payload := []byte("delete version authorization") + + put := func(t *testing.T, object string, versioned bool) string { + t.Helper() + info, err := obj.PutObject(t.Context(), bucket, object, + mustGetPutObjReader(t, bytes.NewReader(payload), int64(len(payload)), "", ""), ObjectOptions{Versioned: versioned}) + if err != nil { + t.Fatal(err) + } + if versioned && info.VersionID == "" { + t.Fatalf("%s: versioned PUT returned an empty version ID", instanceType) + } + return info.VersionID + } + remove := func(t *testing.T, object, versionID string, creds auth.Credentials) *httptest.ResponseRecorder { + t.Helper() + target := getDeleteObjectURL("", bucket, object) + if versionID != "" { + target += "?" + url.Values{xhttp.VersionID: {versionID}}.Encode() + } + req, err := newTestSignedRequestV4(http.MethodDelete, target, 0, nil, creds.AccessKey, creds.SecretKey, nil) + if err != nil { + t.Fatal(err) + } + rec := httptest.NewRecorder() + apiRouter.ServeHTTP(rec, req) + return rec + } + + t.Run("version permission deletes an explicit version", func(t *testing.T) { + object := "delete-authz/version-only-explicit" + versionID := put(t, object, true) + if rec := remove(t, object, versionID, versionOnly); rec.Code != http.StatusNoContent { + t.Fatalf("status %d, want 204: %s", rec.Code, rec.Body.String()) + } + if _, err := obj.GetObjectInfo(t.Context(), bucket, object, ObjectOptions{VersionID: versionID}); !isErrObjectNotFound(err) && !isErrVersionNotFound(err) { + t.Fatalf("explicit version still exists: %v", err) + } + }) + + t.Run("version permission cannot create a delete marker", func(t *testing.T) { + object := "delete-authz/version-only-simple" + versionID := put(t, object, true) + if rec := remove(t, object, "", versionOnly); rec.Code != http.StatusForbidden { + t.Fatalf("status %d, want 403: %s", rec.Code, rec.Body.String()) + } + if _, err := obj.GetObjectInfo(t.Context(), bucket, object, ObjectOptions{VersionID: versionID}); err != nil { + t.Fatalf("denied simple delete removed the version: %v", err) + } + }) + + t.Run("object permission cannot delete an explicit version", func(t *testing.T) { + object := "delete-authz/delete-only-explicit" + versionID := put(t, object, true) + if rec := remove(t, object, versionID, deleteOnly); rec.Code != http.StatusForbidden { + t.Fatalf("status %d, want 403: %s", rec.Code, rec.Body.String()) + } + if _, err := obj.GetObjectInfo(t.Context(), bucket, object, ObjectOptions{VersionID: versionID}); err != nil { + t.Fatalf("denied version delete removed the version: %v", err) + } + }) + + t.Run("object permission creates a delete marker", func(t *testing.T) { + object := "delete-authz/delete-only-simple" + versionID := put(t, object, true) + if rec := remove(t, object, "", deleteOnly); rec.Code != http.StatusNoContent { + t.Fatalf("status %d, want 204: %s", rec.Code, rec.Body.String()) + } + if _, err := obj.GetObjectInfo(t.Context(), bucket, object, ObjectOptions{VersionID: versionID}); err != nil { + t.Fatalf("simple delete removed the old version: %v", err) + } + }) + + t.Run("null is an explicit version", func(t *testing.T) { + object := "delete-authz/null-version" + if versionID := put(t, object, false); versionID != "" { + t.Fatalf("unversioned PUT returned version ID %q", versionID) + } + if rec := remove(t, object, nullVersionID, versionOnly); rec.Code != http.StatusNoContent { + t.Fatalf("status %d, want 204: %s", rec.Code, rec.Body.String()) + } + if _, err := obj.GetObjectInfo(t.Context(), bucket, object, ObjectOptions{VersionID: nullVersionID}); !isErrObjectNotFound(err) && !isErrVersionNotFound(err) { + t.Fatalf("null version still exists: %v", err) + } + }) + + t.Run("authorization precedes invalid version parsing", func(t *testing.T) { + object := "delete-authz/invalid-version" + if rec := remove(t, object, "not-a-uuid", deleteOnly); rec.Code != http.StatusForbidden { + t.Fatalf("delete-only status %d, want 403: %s", rec.Code, rec.Body.String()) + } + if rec := remove(t, object, "not-a-uuid", versionOnly); rec.Code != http.StatusBadRequest { + t.Fatalf("version-only status %d, want 400: %s", rec.Code, rec.Body.String()) + } + }) + + t.Run("padded version uses the effective ID", func(t *testing.T) { + object := "delete-authz/padded-version" + versionID := put(t, object, true) + if rec := remove(t, object, versionID+" ", versionOnly); rec.Code != http.StatusNoContent { + t.Fatalf("status %d, want 204: %s", rec.Code, rec.Body.String()) + } + }) +} + +func TestAPIDeleteObjectVersionDenyAndReplicationCompatibility(t *testing.T) { + defer DetectTestLeak(t)() + ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{ + t: t, + objAPITest: testAPIDeleteObjectVersionDenyAndReplicationCompatibility, + endpoints: []string{"DeleteObject"}, + makeBucketOptions: MakeBucketOptions{VersioningEnabled: true}, + }) +} + +func testAPIDeleteObjectVersionDenyAndReplicationCompatibility(obj ObjectLayer, instanceType, bucket string, + apiRouter http.Handler, _ auth.Credentials, t *testing.T, +) { + payload := []byte("delete version deny compatibility") + put := func(t *testing.T, object string) string { + t.Helper() + info, err := obj.PutObject(t.Context(), bucket, object, + mustGetPutObjReader(t, bytes.NewReader(payload), int64(len(payload)), "", ""), ObjectOptions{Versioned: true}) + if err != nil { + t.Fatal(err) + } + return info.VersionID + } + request := func(t *testing.T, object, versionID string, creds auth.Credentials, replicationRequest bool) *httptest.ResponseRecorder { + t.Helper() + target := getDeleteObjectURL("", bucket, object) + if versionID != "" { + target += "?" + url.Values{xhttp.VersionID: {versionID}}.Encode() + } + var headers map[string]string + if replicationRequest { + headers = map[string]string{ + xhttp.MinIOSourceReplicationRequest: "true", + xhttp.AmzBucketReplicationStatus: "REPLICA", + xhttp.MinIOSourceDeleteMarker: "false", + xhttp.MinIOSourceMTime: UTCNow().Format(time.RFC3339Nano), + } + } + req, err := newTestSignedRequestV4(http.MethodDelete, target, 0, nil, creds.AccessKey, creds.SecretKey, headers) + if err != nil { + t.Fatal(err) + } + rec := httptest.NewRecorder() + apiRouter.ServeHTTP(rec, req) + return rec + } + + minimal := newDeleteAuthzPolicyUser(t, instanceType, bucket, `[ + {"Effect":"Allow","Action":["s3:DeleteObject","s3:ReplicateDelete"],"Resource":["arn:aws:s3:::`+bucket+`/*"]} + ]`) + denied := newDeleteAuthzPolicyUser(t, instanceType, bucket, `[ + {"Effect":"Allow","Action":["s3:DeleteObject","s3:DeleteObjectVersion","s3:ReplicateDelete"],"Resource":["arn:aws:s3:::`+bucket+`/*"]}, + {"Effect":"Deny","Action":"s3:DeleteObjectVersion","Resource":"arn:aws:s3:::`+bucket+`/deny/*"} + ]`) + deleteOnly := newObjectAttributesAuthzUser(t, instanceType, bucket, `"s3:DeleteObject"`) + + t.Run("ordinary explicit deny wins", func(t *testing.T) { + object := "deny/ordinary" + versionID := put(t, object) + if rec := request(t, object, versionID, denied, false); rec.Code != http.StatusForbidden { + t.Fatalf("status %d, want 403: %s", rec.Code, rec.Body.String()) + } + }) + + t.Run("version deny does not block a simple delete", func(t *testing.T) { + object := "deny/simple" + put(t, object) + if rec := request(t, object, "", denied, false); rec.Code != http.StatusNoContent { + t.Fatalf("status %d, want 204: %s", rec.Code, rec.Body.String()) + } + }) + + t.Run("replication keeps minimal target policy", func(t *testing.T) { + object := "replication/minimal" + versionID := put(t, object) + if rec := request(t, object, versionID, minimal, true); rec.Code != http.StatusNoContent { + t.Fatalf("status %d, want 204: %s", rec.Code, rec.Body.String()) + } + }) + + t.Run("replication preserves explicit version deny", func(t *testing.T) { + object := "deny/replication" + versionID := put(t, object) + if rec := request(t, object, versionID, denied, true); rec.Code != http.StatusForbidden { + t.Fatalf("status %d, want 403: %s", rec.Code, rec.Body.String()) + } + }) + + t.Run("marker alone cannot enter the replication path", func(t *testing.T) { + object := "replication/fake-marker" + versionID := put(t, object) + target := getDeleteObjectURL("", bucket, object) + "?" + url.Values{xhttp.VersionID: {versionID}}.Encode() + req, err := newTestSignedRequestV4(http.MethodDelete, target, 0, nil, deleteOnly.AccessKey, deleteOnly.SecretKey, + map[string]string{xhttp.MinIOSourceReplicationRequest: "true"}) + if err != nil { + t.Fatal(err) + } + rec := httptest.NewRecorder() + apiRouter.ServeHTTP(rec, req) + if rec.Code != http.StatusForbidden { + t.Fatalf("status %d, want 403: %s", rec.Code, rec.Body.String()) + } + if _, err = obj.GetObjectInfo(t.Context(), bucket, object, ObjectOptions{VersionID: versionID}); err != nil { + t.Fatalf("fake marker removed the version: %v", err) + } + }) +} + +func newDeleteAuthzPolicyUser(t *testing.T, instanceType, bucket, statements string) auth.Credentials { + t.Helper() + accessKey, secretKey, err := auth.GenerateCredentials() + if err != nil { + t.Fatalf("%s: generate credentials: %v", instanceType, err) + } + creds := auth.Credentials{AccessKey: accessKey, SecretKey: secretKey} + if _, err = globalIAMSys.CreateUser(t.Context(), accessKey, madmin.AddOrUpdateUserReq{ + SecretKey: secretKey, + Status: madmin.AccountEnabled, + }); err != nil { + t.Fatalf("%s: create delete authz user: %v", instanceType, err) + } + policyJSON := `{"Version":"2012-10-17","Statement":` + statements + `}` + parsed, err := policy.ParseConfig(strings.NewReader(policyJSON)) + if err != nil { + t.Fatalf("%s: parse delete authz policy: %v", instanceType, err) + } + policyName := "delete-version-authz-" + mustGetUUID() + if _, err = globalIAMSys.SetPolicy(t.Context(), policyName, *parsed); err != nil { + t.Fatalf("%s: install delete authz policy: %v", instanceType, err) + } + if _, err = globalIAMSys.PolicyDBSet(t.Context(), accessKey, policyName, regUser, false); err != nil { + t.Fatalf("%s: attach delete authz policy: %v", instanceType, err) + } + return creds +} diff --git a/cmd/dummy-handlers.go b/cmd/dummy-handlers.go index 685b79256..3713f7c39 100644 --- a/cmd/dummy-handlers.go +++ b/cmd/dummy-handlers.go @@ -22,7 +22,7 @@ import ( "github.com/minio/minio/internal/logger" "github.com/minio/mux" - "github.com/minio/pkg/v3/policy" + "github.com/pgsty/silo-pkg/v3/policy" ) // Data types used for returning dummy tagging XML. @@ -165,93 +165,3 @@ func (api objectAPIHandlers) GetBucketLoggingHandler(w http.ResponseWriter, r *h func (api objectAPIHandlers) DeleteBucketWebsiteHandler(w http.ResponseWriter, r *http.Request) { writeSuccessResponseHeadersOnly(w) } - -// GetBucketCorsHandler - GET bucket cors, a dummy api -func (api objectAPIHandlers) GetBucketCorsHandler(w http.ResponseWriter, r *http.Request) { - ctx := newContext(r, w, "GetBucketCors") - - defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r)) - - vars := mux.Vars(r) - bucket := vars["bucket"] - - objAPI := api.ObjectAPI() - if objAPI == nil { - writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL) - return - } - - if s3Error := checkRequestAuthType(ctx, r, policy.GetBucketCorsAction, bucket, ""); s3Error != ErrNone { - writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL) - return - } - - // Validate if bucket exists, before proceeding further... - _, err := objAPI.GetBucketInfo(ctx, bucket, BucketOptions{}) - if err != nil { - writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL) - return - } - - writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrNoSuchCORSConfiguration), r.URL) -} - -// PutBucketCorsHandler - PUT bucket cors, a dummy api -func (api objectAPIHandlers) PutBucketCorsHandler(w http.ResponseWriter, r *http.Request) { - ctx := newContext(r, w, "PutBucketCors") - - defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r)) - - vars := mux.Vars(r) - bucket := vars["bucket"] - - objAPI := api.ObjectAPI() - if objAPI == nil { - writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL) - return - } - - if s3Error := checkRequestAuthType(ctx, r, policy.PutBucketCorsAction, bucket, ""); s3Error != ErrNone { - writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL) - return - } - - // Validate if bucket exists, before proceeding further... - _, err := objAPI.GetBucketInfo(ctx, bucket, BucketOptions{}) - if err != nil { - writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL) - return - } - - writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrNotImplemented), r.URL) -} - -// DeleteBucketCorsHandler - DELETE bucket cors, a dummy api -func (api objectAPIHandlers) DeleteBucketCorsHandler(w http.ResponseWriter, r *http.Request) { - ctx := newContext(r, w, "DeleteBucketCors") - - defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r)) - - vars := mux.Vars(r) - bucket := vars["bucket"] - - objAPI := api.ObjectAPI() - if objAPI == nil { - writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL) - return - } - - if s3Error := checkRequestAuthType(ctx, r, policy.DeleteBucketCorsAction, bucket, ""); s3Error != ErrNone { - writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL) - return - } - - // Validate if bucket exists, before proceeding further... - _, err := objAPI.GetBucketInfo(ctx, bucket, BucketOptions{}) - if err != nil { - writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL) - return - } - - writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrNotImplemented), r.URL) -} diff --git a/cmd/dynamic-timeouts_test.go b/cmd/dynamic-timeouts_test.go index b353b983b..dd0e306c2 100644 --- a/cmd/dynamic-timeouts_test.go +++ b/cmd/dynamic-timeouts_test.go @@ -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() }) } diff --git a/cmd/encryption-v1.go b/cmd/encryption-v1.go index c3da051a8..32710915c 100644 --- a/cmd/encryption-v1.go +++ b/cmd/encryption-v1.go @@ -551,6 +551,24 @@ func DecryptCopyRequestR(client io.Reader, h http.Header, bucket, object string, return newDecryptReader(client, key, bucket, object, seqNumber, metadata) } +// checkSSECReadKey authenticates a supplied SSE-C read key against the sealed +// object key when a read has no data from which to build a decryptor. +func checkSSECReadKey(h http.Header, oi ObjectInfo, opts ObjectOptions) error { + if opts.NoDecryption || opts.Transition.RestoreRequest != nil || !crypto.SSEC.IsEncrypted(oi.UserDefined) { + return nil + } + switch { + case crypto.SSECopy.IsRequested(h): + _, err := crypto.SSECopy.UnsealObjectKey(h, oi.UserDefined, oi.Bucket, oi.Name) + return err + case crypto.SSEC.IsRequested(h): + _, err := crypto.SSEC.UnsealObjectKey(h, oi.UserDefined, oi.Bucket, oi.Name) + return err + default: + return nil + } +} + func newDecryptReader(client io.Reader, key []byte, bucket, object string, seqNumber uint32, metadata map[string]string) (io.Reader, error) { objectEncryptionKey, err := decryptObjectMeta(key, bucket, object, metadata) if err != nil { @@ -1008,7 +1026,7 @@ func DecryptObjectInfo(info *ObjectInfo, r *http.Request) (encrypted bool, err e if encrypted { if crypto.SSEC.IsEncrypted(info.UserDefined) { if !crypto.SSEC.IsRequested(headers) && !crypto.SSECopy.IsRequested(headers) { - if r.Header.Get(xhttp.MinIOSourceReplicationRequest) != "true" { + if !isReplicaTrusted(r.Context()) { return encrypted, errEncryptedObject } } diff --git a/cmd/endpoint-ellipses.go b/cmd/endpoint-ellipses.go index 627eada09..83ba8748b 100644 --- a/cmd/endpoint-ellipses.go +++ b/cmd/endpoint-ellipses.go @@ -28,8 +28,8 @@ import ( "github.com/cespare/xxhash/v2" "github.com/minio/minio-go/v7/pkg/set" "github.com/minio/minio/internal/config" - "github.com/minio/pkg/v3/ellipses" - "github.com/minio/pkg/v3/env" + "github.com/pgsty/silo-pkg/v3/ellipses" + "github.com/pgsty/silo-pkg/v3/env" ) // This file implements and supports ellipses pattern for diff --git a/cmd/endpoint-ellipses_test.go b/cmd/endpoint-ellipses_test.go index 6caaebbb6..58b478c73 100644 --- a/cmd/endpoint-ellipses_test.go +++ b/cmd/endpoint-ellipses_test.go @@ -22,7 +22,7 @@ import ( "reflect" "testing" - "github.com/minio/pkg/v3/ellipses" + "github.com/pgsty/silo-pkg/v3/ellipses" ) // Tests create endpoints with ellipses and without. diff --git a/cmd/endpoint.go b/cmd/endpoint.go index 56a791792..c7b51ce72 100644 --- a/cmd/endpoint.go +++ b/cmd/endpoint.go @@ -37,8 +37,8 @@ import ( "github.com/minio/minio/internal/config" "github.com/minio/minio/internal/logger" "github.com/minio/minio/internal/mountinfo" - "github.com/minio/pkg/v3/env" - xnet "github.com/minio/pkg/v3/net" + "github.com/pgsty/silo-pkg/v3/env" + xnet "github.com/pgsty/silo-pkg/v3/net" ) // EndpointType - enum for endpoint type. diff --git a/cmd/erasure-healing.go b/cmd/erasure-healing.go index 9ea507f7a..8afc126ca 100644 --- a/cmd/erasure-healing.go +++ b/cmd/erasure-healing.go @@ -31,7 +31,7 @@ import ( "github.com/minio/madmin-go/v3" "github.com/minio/minio/internal/grid" "github.com/minio/minio/internal/logger" - "github.com/minio/pkg/v3/sync/errgroup" + "github.com/pgsty/silo-pkg/v3/sync/errgroup" "github.com/puzpuzpuz/xsync/v3" ) diff --git a/cmd/erasure-metadata-utils.go b/cmd/erasure-metadata-utils.go index 1409d99ec..cb7087c8b 100644 --- a/cmd/erasure-metadata-utils.go +++ b/cmd/erasure-metadata-utils.go @@ -23,7 +23,7 @@ import ( "errors" "hash/crc32" - "github.com/minio/pkg/v3/sync/errgroup" + "github.com/pgsty/silo-pkg/v3/sync/errgroup" ) // counterMap type adds GetValueWithQuorum method to a map[T]int used to count occurrences of values of type T. diff --git a/cmd/erasure-metadata.go b/cmd/erasure-metadata.go index 47cfe5d29..1c3209f24 100644 --- a/cmd/erasure-metadata.go +++ b/cmd/erasure-metadata.go @@ -31,7 +31,7 @@ import ( "github.com/minio/minio/internal/crypto" "github.com/minio/minio/internal/hash/sha256" xhttp "github.com/minio/minio/internal/http" - "github.com/minio/pkg/v3/sync/errgroup" + "github.com/pgsty/silo-pkg/v3/sync/errgroup" ) // Object was stored with additional erasure codes due to degraded system at upload time diff --git a/cmd/erasure-multipart-fullobject_test.go b/cmd/erasure-multipart-fullobject_test.go index eaba82bb6..bb196acda 100644 --- a/cmd/erasure-multipart-fullobject_test.go +++ b/cmd/erasure-multipart-fullobject_test.go @@ -24,6 +24,7 @@ import ( "net/http" "net/http/httptest" "strconv" + "strings" "testing" "github.com/dustin/go-humanize" @@ -139,13 +140,18 @@ func completeMultipartUploadHTTP(t *testing.T, apiRouter http.Handler, creds aut return rec } -func apiErrorCode(t *testing.T, rec *httptest.ResponseRecorder) string { +func apiError(t *testing.T, rec *httptest.ResponseRecorder) APIErrorResponse { t.Helper() var e APIErrorResponse if err := xml.Unmarshal(rec.Body.Bytes(), &e); err != nil { t.Fatalf("unable to decode error response %q: %v", rec.Body.String(), err) } - return e.Code + return e +} + +func apiErrorCode(t *testing.T, rec *httptest.ResponseRecorder) string { + t.Helper() + return apiError(t, rec).Code } // TestAPICompleteMultipartFullObjectChecksum covers pgsty/silo#31. @@ -243,12 +249,12 @@ func testAPICompleteMultipartFullObjectChecksumMismatch(obj ObjectLayer, instanc t.Fatalf("%s: CompleteMultipartUpload with a bad full object checksum returned %d, want 400", instanceType, rec.Code) } - // NOTE: AWS S3 documents BadDigest for a full object checksum mismatch on - // CompleteMultipartUpload. MinIO reports XAmzContentChecksumMismatch. That - // deviation is tracked separately; assert the current code so a future - // change to it is a deliberate one. - if got := apiErrorCode(t, rec); got != "XAmzContentChecksumMismatch" { - t.Fatalf("%s: expected XAmzContentChecksumMismatch, got %q", instanceType, got) + apiErr := apiError(t, rec) + if apiErr.Code != "BadDigest" { + t.Fatalf("%s: expected BadDigest, got %q", instanceType, apiErr.Code) + } + if want := "The CRC32 checksum you specified did not match the calculated checksum."; apiErr.Message != want { + t.Fatalf("%s: expected message %q, got %q", instanceType, want, apiErr.Message) } if _, err := obj.GetObjectInfo(t.Context(), bucketName, objectName, ObjectOptions{}); err == nil { @@ -288,8 +294,33 @@ func testAPICompleteMultipartCompositeStillRequiresPartChecksums(obj ObjectLayer t.Fatalf("%s/%s: composite CompleteMultipartUpload without part checksums returned %d, want 400", instanceType, typ.String(), rec.Code) } - if got := apiErrorCode(t, rec); got != "InvalidPart" { - t.Fatalf("%s/%s: expected InvalidPart, got %q", instanceType, typ.String(), got) + apiErr := apiError(t, rec) + if apiErr.Code != "InvalidRequest" { + t.Fatalf("%s/%s: expected InvalidRequest, got %q", instanceType, typ.String(), apiErr.Code) + } + wantMessage := fmt.Sprintf("The upload was created using a %s checksum. The complete request must include the checksum for each part. It was missing for part 1 in the request.", strings.ToLower(typ.String())) + if apiErr.Message != wantMessage { + t.Fatalf("%s/%s: expected message %q, got %q", instanceType, typ.String(), wantMessage, apiErr.Message) + } + + // A retry that supplies part 1 but omits part 2 must name the actual + // missing part, not merely the first part in the upload. + completedParts := []CompletePart{ + completePartWithChecksum(typ, 1, etags[0], mustChecksum(t, typ, partData[0])), + {PartNumber: 2, ETag: etags[1]}, + } + rec = completePartsHTTP(t, apiRouter, credentials, bucketName, objectName, uploadID, completedParts, nil) + if rec.Code != http.StatusBadRequest { + t.Fatalf("%s/%s: composite completion missing part 2 checksum returned %d, want 400", + instanceType, typ.String(), rec.Code) + } + apiErr = apiError(t, rec) + if apiErr.Code != "InvalidRequest" { + t.Fatalf("%s/%s: expected InvalidRequest, got %q", instanceType, typ.String(), apiErr.Code) + } + wantMessage = fmt.Sprintf("The upload was created using a %s checksum. The complete request must include the checksum for each part. It was missing for part 2 in the request.", strings.ToLower(typ.String())) + if apiErr.Message != wantMessage { + t.Fatalf("%s/%s: expected message %q, got %q", instanceType, typ.String(), wantMessage, apiErr.Message) } if _, err := obj.GetObjectInfo(t.Context(), bucketName, objectName, ObjectOptions{}); err == nil { t.Fatalf("%s/%s: object was created despite a rejected completion", instanceType, typ.String()) @@ -297,6 +328,230 @@ func testAPICompleteMultipartCompositeStillRequiresPartChecksums(obj ObjectLayer } } +// TestAPICompleteMultipartCompositeChecksumMismatch covers the composite +// object-checksum path independently from full object checksum merging. +func TestAPICompleteMultipartCompositeChecksumMismatch(t *testing.T) { + defer DetectTestLeak(t)() + ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{ + t: t, + objAPITest: testAPICompleteMultipartCompositeChecksumMismatch, + endpoints: []string{"NewMultipart", "PutObjectPart", "CompleteMultipart"}, + }) +} + +func testAPICompleteMultipartCompositeChecksumMismatch(obj ObjectLayer, instanceType, bucketName string, apiRouter http.Handler, + credentials auth.Credentials, t *testing.T, +) { + typ := hash.ChecksumCRC32 + partData, _ := multipartChecksumTestData() + objectName := "uploads/composite-object-mismatch" + uploadID := newMultipartUploadHTTP(t, apiRouter, credentials, bucketName, objectName, + typ.String(), xhttp.AmzChecksumTypeComposite) + etags := uploadPartsHTTP(t, apiRouter, credentials, bucketName, objectName, uploadID, typ, partData) + partCS := []string{mustChecksum(t, typ, partData[0]), mustChecksum(t, typ, partData[1])} + rec := completeMultipartUploadHTTP(t, apiRouter, credentials, bucketName, objectName, uploadID, etags, partCS, + map[string]string{ + typ.Key(): mustChecksum(t, typ, []byte("wrong composite checksum")) + "-2", + xhttp.AmzChecksumType: xhttp.AmzChecksumTypeComposite, + }) + if rec.Code != http.StatusBadRequest { + t.Fatalf("%s: composite checksum mismatch returned %d, want 400", instanceType, rec.Code) + } + apiErr := apiError(t, rec) + if apiErr.Code != "BadDigest" { + t.Fatalf("%s: expected BadDigest, got %q", instanceType, apiErr.Code) + } + if want := "The CRC32 checksum you specified did not match the calculated checksum."; apiErr.Message != want { + t.Fatalf("%s: expected message %q, got %q", instanceType, want, apiErr.Message) + } + if _, err := obj.GetObjectInfo(t.Context(), bucketName, objectName, ObjectOptions{}); err == nil { + t.Fatalf("%s: object was created despite a failed composite checksum validation", instanceType) + } +} + +// TestAPICompleteMultipartChecksumTypeMismatch verifies the type comparison in +// both directions. ChecksumType is a bitmask, so a containment check alone +// incorrectly accepts COMPOSITE uploads completed as FULL_OBJECT. +func TestAPICompleteMultipartChecksumTypeMismatch(t *testing.T) { + defer DetectTestLeak(t)() + ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{ + t: t, + objAPITest: testAPICompleteMultipartChecksumTypeMismatch, + endpoints: []string{"NewMultipart", "PutObjectPart", "CompleteMultipart"}, + }) +} + +func testAPICompleteMultipartChecksumTypeMismatch(obj ObjectLayer, instanceType, bucketName string, apiRouter http.Handler, + credentials auth.Credentials, t *testing.T, +) { + typ := hash.ChecksumCRC32 + partData, full := multipartChecksumTestData() + for _, test := range []struct { + name string + createdType string + providedType string + }{ + {name: "full-to-composite", createdType: xhttp.AmzChecksumTypeFullObject, providedType: xhttp.AmzChecksumTypeComposite}, + {name: "composite-to-full", createdType: xhttp.AmzChecksumTypeComposite, providedType: xhttp.AmzChecksumTypeFullObject}, + } { + t.Run(test.name, func(t *testing.T) { + objectName := "type-mismatch/" + test.name + uploadID := newMultipartUploadHTTP(t, apiRouter, credentials, bucketName, objectName, + typ.String(), test.createdType) + etags := uploadPartsHTTP(t, apiRouter, credentials, bucketName, objectName, uploadID, typ, partData) + partCS := []string{mustChecksum(t, typ, partData[0]), mustChecksum(t, typ, partData[1])} + rec := completeMultipartUploadHTTP(t, apiRouter, credentials, bucketName, objectName, uploadID, etags, partCS, + map[string]string{ + typ.Key(): mustChecksum(t, typ, full), + xhttp.AmzChecksumType: test.providedType, + }) + if rec.Code != http.StatusBadRequest { + t.Fatalf("%s: checksum type mismatch returned %d, want 400", instanceType, rec.Code) + } + apiErr := apiError(t, rec) + if apiErr.Code != "BadDigest" { + t.Fatalf("%s: expected BadDigest, got %q", instanceType, apiErr.Code) + } + wantMessage := fmt.Sprintf("The checksum type %s does not match the multipart upload checksum type %s.", test.providedType, test.createdType) + if apiErr.Message != wantMessage { + t.Fatalf("%s: expected message %q, got %q", instanceType, wantMessage, apiErr.Message) + } + if _, err := obj.GetObjectInfo(t.Context(), bucketName, objectName, ObjectOptions{}); err == nil { + t.Fatalf("%s: object was created despite a rejected checksum type", instanceType) + } + }) + + t.Run(test.name+"-type-only", func(t *testing.T) { + objectName := "type-mismatch/type-only-" + test.name + uploadID := newMultipartUploadHTTP(t, apiRouter, credentials, bucketName, objectName, + typ.String(), test.createdType) + etags := uploadPartsHTTP(t, apiRouter, credentials, bucketName, objectName, uploadID, typ, partData) + partCS := []string{mustChecksum(t, typ, partData[0]), mustChecksum(t, typ, partData[1])} + rec := completeMultipartUploadHTTP(t, apiRouter, credentials, bucketName, objectName, uploadID, etags, partCS, + map[string]string{xhttp.AmzChecksumType: test.providedType}) + if rec.Code != http.StatusBadRequest { + t.Fatalf("%s: checksum type-only mismatch returned %d, want 400", instanceType, rec.Code) + } + apiErr := apiError(t, rec) + if apiErr.Code != "BadDigest" { + t.Fatalf("%s: expected BadDigest, got %q", instanceType, apiErr.Code) + } + wantMessage := fmt.Sprintf("The checksum type %s does not match the multipart upload checksum type %s.", test.providedType, test.createdType) + if apiErr.Message != wantMessage { + t.Fatalf("%s: expected message %q, got %q", instanceType, wantMessage, apiErr.Message) + } + if _, err := obj.GetObjectInfo(t.Context(), bucketName, objectName, ObjectOptions{}); err == nil { + t.Fatalf("%s: object was created despite a rejected checksum type-only assertion", instanceType) + } + }) + } + + for _, test := range []struct { + name string + providedType string + withChecksum bool + }{ + {name: "unknown-type-only", providedType: "NOT_A_TYPE"}, + {name: "lowercase-type-only", providedType: "full_object"}, + {name: "unknown-with-checksum", providedType: "NOT_A_TYPE", withChecksum: true}, + } { + t.Run(test.name, func(t *testing.T) { + objectName := "type-mismatch/invalid-" + test.name + uploadID := newMultipartUploadHTTP(t, apiRouter, credentials, bucketName, objectName, + typ.String(), xhttp.AmzChecksumTypeComposite) + etags := uploadPartsHTTP(t, apiRouter, credentials, bucketName, objectName, uploadID, typ, partData) + partCS := []string{mustChecksum(t, typ, partData[0]), mustChecksum(t, typ, partData[1])} + headers := map[string]string{xhttp.AmzChecksumType: test.providedType} + if test.withChecksum { + headers[typ.Key()] = mustChecksum(t, typ, full) + } + rec := completeMultipartUploadHTTP(t, apiRouter, credentials, bucketName, objectName, uploadID, etags, partCS, headers) + if rec.Code != http.StatusBadRequest || apiErrorCode(t, rec) != "InvalidArgument" { + t.Fatalf("%s: invalid checksum type returned %d %s", instanceType, rec.Code, rec.Body.String()) + } + if _, err := obj.GetObjectInfo(t.Context(), bucketName, objectName, ObjectOptions{}); err == nil { + t.Fatalf("%s: object was created despite an invalid checksum type", instanceType) + } + }) + } + + t.Run("matching-type-only", func(t *testing.T) { + objectName := "type-mismatch/matching-type-only" + uploadID := newMultipartUploadHTTP(t, apiRouter, credentials, bucketName, objectName, + typ.String(), xhttp.AmzChecksumTypeComposite) + etags := uploadPartsHTTP(t, apiRouter, credentials, bucketName, objectName, uploadID, typ, partData) + partCS := []string{mustChecksum(t, typ, partData[0]), mustChecksum(t, typ, partData[1])} + rec := completeMultipartUploadHTTP(t, apiRouter, credentials, bucketName, objectName, uploadID, etags, partCS, + map[string]string{xhttp.AmzChecksumType: xhttp.AmzChecksumTypeComposite}) + if rec.Code != http.StatusOK { + t.Fatalf("%s: matching checksum type-only assertion returned %d %s", instanceType, rec.Code, rec.Body.String()) + } + }) + + t.Run("omitted-type-is-not-composite", func(t *testing.T) { + objectName := "type-mismatch/omitted-type" + uploadID := newMultipartUploadHTTP(t, apiRouter, credentials, bucketName, objectName, + typ.String(), xhttp.AmzChecksumTypeFullObject) + etags := uploadPartsHTTP(t, apiRouter, credentials, bucketName, objectName, uploadID, typ, partData) + rec := completeMultipartUploadHTTP(t, apiRouter, credentials, bucketName, objectName, uploadID, etags, nil, + map[string]string{typ.Key(): mustChecksum(t, typ, full)}) + if rec.Code != http.StatusOK { + t.Fatalf("%s: completion without an explicit checksum type returned %d %s", + instanceType, rec.Code, rec.Body.String()) + } + }) + + t.Run("algorithm-mismatch-remains-invalid-argument", func(t *testing.T) { + objectName := "type-mismatch/algorithm" + uploadID := newMultipartUploadHTTP(t, apiRouter, credentials, bucketName, objectName, + typ.String(), xhttp.AmzChecksumTypeFullObject) + etags := uploadPartsHTTP(t, apiRouter, credentials, bucketName, objectName, uploadID, typ, partData) + rec := completeMultipartUploadHTTP(t, apiRouter, credentials, bucketName, objectName, uploadID, etags, nil, + map[string]string{ + hash.ChecksumCRC32C.Key(): mustChecksum(t, hash.ChecksumCRC32C, full), + xhttp.AmzChecksumType: xhttp.AmzChecksumTypeFullObject, + }) + if rec.Code != http.StatusBadRequest || apiErrorCode(t, rec) != "InvalidArgument" { + t.Fatalf("%s: algorithm mismatch returned %d %s", instanceType, rec.Code, rec.Body.String()) + } + }) + + t.Run("crc64nvme-composite-is-rejected", func(t *testing.T) { + crc64Type := hash.ChecksumCRC64NVME + objectName := "type-mismatch/crc64nvme-composite" + req, err := newTestSignedRequestV4(http.MethodPost, getNewMultipartURL("", bucketName, objectName), + 0, nil, credentials.AccessKey, credentials.SecretKey, map[string]string{ + xhttp.AmzChecksumAlgo: crc64Type.String(), + xhttp.AmzChecksumType: xhttp.AmzChecksumTypeComposite, + }) + if err != nil { + t.Fatal(err) + } + rec := httptest.NewRecorder() + apiRouter.ServeHTTP(rec, req) + if rec.Code != http.StatusBadRequest || apiErrorCode(t, rec) != "InvalidArgument" { + t.Fatalf("%s: CRC64NVME/COMPOSITE returned %d %s", instanceType, rec.Code, rec.Body.String()) + } + }) + + t.Run("crc64nvme-composite-completion-is-rejected", func(t *testing.T) { + crc64Type := hash.ChecksumCRC64NVME + objectName := "type-mismatch/crc64nvme-composite-completion" + uploadID := newMultipartUploadHTTP(t, apiRouter, credentials, bucketName, objectName, + crc64Type.String(), xhttp.AmzChecksumTypeFullObject) + etags := uploadPartsHTTP(t, apiRouter, credentials, bucketName, objectName, uploadID, crc64Type, partData) + partCS := []string{mustChecksum(t, crc64Type, partData[0]), mustChecksum(t, crc64Type, partData[1])} + rec := completeMultipartUploadHTTP(t, apiRouter, credentials, bucketName, objectName, uploadID, etags, partCS, + map[string]string{xhttp.AmzChecksumType: xhttp.AmzChecksumTypeComposite}) + if rec.Code != http.StatusBadRequest || apiErrorCode(t, rec) != "BadDigest" { + t.Fatalf("%s: CRC64NVME composite completion returned %d %s", instanceType, rec.Code, rec.Body.String()) + } + if _, err := obj.GetObjectInfo(t.Context(), bucketName, objectName, ObjectOptions{}); err == nil { + t.Fatalf("%s: object was created despite a rejected CRC64NVME checksum type", instanceType) + } + }) +} + // TestAPICompleteMultipartFullObjectVariants pins down the surrounding // behavior of the relaxation: what may be omitted, what must still match, and // that a zero length object is handled like any other. diff --git a/cmd/erasure-multipart-ssec-replica_test.go b/cmd/erasure-multipart-ssec-replica_test.go new file mode 100644 index 000000000..4a09f246f --- /dev/null +++ b/cmd/erasure-multipart-ssec-replica_test.go @@ -0,0 +1,790 @@ +// Copyright (c) 2015-2026 MinIO, Inc. +// +// This file is part of MinIO Object Storage stack +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package cmd + +import ( + "bytes" + "crypto/md5" + "encoding/base64" + "encoding/xml" + "fmt" + "io" + "net/http" + "net/http/httptest" + "strconv" + "strings" + "testing" + "time" + + "github.com/minio/minio/internal/auth" + "github.com/minio/minio/internal/crypto" + "github.com/minio/minio/internal/hash" + xhttp "github.com/minio/minio/internal/http" + "github.com/minio/sio" +) + +// TestAPISSECReplicaPartNumberReads replicates a three-part SSE-C multipart +// object through the trusted-replication write path and compares what +// GET ?partNumber=N returns before and after the replica overwrite. +func TestAPISSECReplicaPartNumberReads(t *testing.T) { + defer DetectTestLeak(t)() + ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{ + t: t, + objAPITest: testAPISSECReplicaPartNumberReads, + }) +} + +func testAPISSECReplicaPartNumberReads(obj ObjectLayer, instanceType, bucketName string, + apiRouter http.Handler, credentials auth.Credentials, t *testing.T, +) { + previousTLS := globalIsTLS + globalIsTLS = true + defer func() { globalIsTLS = previousTLS }() + + replicator := newObjectAttributesAuthzUser(t, instanceType, bucketName, `"s3:PutObject","s3:GetObject","s3:ReplicateObject"`) + key := bytes.Repeat([]byte{0x42}, 32) + keyMD5 := md5.Sum(key) + sseHeaders := map[string]string{ + xhttp.AmzServerSideEncryptionCustomerAlgorithm: xhttp.AmzEncryptionAES, + xhttp.AmzServerSideEncryptionCustomerKey: base64.StdEncoding.EncodeToString(key), + xhttp.AmzServerSideEncryptionCustomerKeyMD5: base64.StdEncoding.EncodeToString(keyMD5[:]), + } + + const mib = 1024 * 1024 + partLens := []int{5 * mib, 5 * mib, 1 * mib} + plaintext := make([]byte, 0, 11*mib) + partData := make([][]byte, len(partLens)) + for i, n := range partLens { + b := make([]byte, n) + for j := range b { + // Distinct, position-dependent bytes so an off-by-N shift is visible. + b[j] = byte(i*7 + j%251) + } + partData[i] = b + plaintext = append(plaintext, b...) + } + + object := "ssec-mp-3part" + + // ---- 1. Build the source: a real three-part SSE-C multipart object. ---- + newRec := httptest.NewRecorder() + newReq, err := newTestSignedRequestV4(http.MethodPost, getNewMultipartURL("", bucketName, object), + 0, nil, credentials.AccessKey, credentials.SecretKey, sseHeaders) + if err != nil { + t.Fatal(err) + } + apiRouter.ServeHTTP(newRec, newReq) + if newRec.Code != http.StatusOK { + t.Fatalf("source NewMultipart status %d: %s", newRec.Code, newRec.Body.String()) + } + var srcInit InitiateMultipartUploadResponse + if err = xmlDecoder(newRec.Body, &srcInit, int64(newRec.Body.Len())); err != nil { + t.Fatal(err) + } + + srcParts := make([]CompletePart, len(partLens)) + for i, b := range partData { + pn := strconv.Itoa(i + 1) + partReq, err := newTestSignedRequestV4(http.MethodPut, + getPutObjectPartURL("", bucketName, object, srcInit.UploadID, pn), + int64(len(b)), bytes.NewReader(b), credentials.AccessKey, credentials.SecretKey, sseHeaders) + if err != nil { + t.Fatal(err) + } + rec := httptest.NewRecorder() + apiRouter.ServeHTTP(rec, partReq) + if rec.Code != http.StatusOK { + t.Fatalf("source PutPart %s status %d: %s", pn, rec.Code, rec.Body.String()) + } + srcParts[i] = CompletePart{PartNumber: i + 1, ETag: canonicalizeETag(rec.Header()[xhttp.ETag][0])} + } + srcCompleteBody, err := xml.Marshal(CompleteMultipartUpload{Parts: srcParts}) + if err != nil { + t.Fatal(err) + } + completeReq, err := newTestSignedRequestV4(http.MethodPost, + getCompleteMultipartUploadURL("", bucketName, object, srcInit.UploadID), int64(len(srcCompleteBody)), + bytes.NewReader(srcCompleteBody), credentials.AccessKey, credentials.SecretKey, sseHeaders) + if err != nil { + t.Fatal(err) + } + completeRec := httptest.NewRecorder() + apiRouter.ServeHTTP(completeRec, completeReq) + if completeRec.Code != http.StatusOK { + t.Fatalf("source Complete status %d: %s", completeRec.Code, completeRec.Body.String()) + } + + // ---- 2. Record the source's per-part metadata and per-part GET answers. ---- + srcOI, err := obj.GetObjectInfo(t.Context(), bucketName, object, ObjectOptions{}) + if err != nil { + t.Fatal(err) + } + t.Logf("[%s] SOURCE parts:", instanceType) + for _, p := range srcOI.Parts { + t.Logf(" part %d Size=%d ActualSize=%d", p.Number, p.Size, p.ActualSize) + } + srcActual, err := srcOI.GetActualSize() + if err != nil { + t.Fatal(err) + } + t.Logf("[%s] SOURCE object Size=%d GetActualSize=%d actual-size-meta=%q", + instanceType, srcOI.Size, srcActual, srcOI.UserDefined[ReservedMetadataPrefix+"actual-size"]) + + type getResult struct { + status int + clen string + crange string + body []byte + } + doGet := func(query string, extra map[string]string) getResult { + hdrs := map[string]string{} + for k, v := range sseHeaders { + hdrs[k] = v + } + for k, v := range extra { + hdrs[k] = v + } + u := getGetObjectURL("", bucketName, object) + query + req, err := newTestSignedRequestV4(http.MethodGet, u, 0, nil, credentials.AccessKey, credentials.SecretKey, hdrs) + if err != nil { + t.Fatal(err) + } + rec := httptest.NewRecorder() + apiRouter.ServeHTTP(rec, req) + return getResult{ + status: rec.Code, + clen: rec.Header().Get(xhttp.ContentLength), + crange: rec.Header().Get(xhttp.ContentRange), + body: append([]byte(nil), rec.Body.Bytes()...), + } + } + + doHead := func(query string) getResult { + u := getGetObjectURL("", bucketName, object) + query + req, err := newTestSignedRequestV4(http.MethodHead, u, 0, nil, credentials.AccessKey, credentials.SecretKey, sseHeaders) + if err != nil { + t.Fatal(err) + } + rec := httptest.NewRecorder() + apiRouter.ServeHTTP(rec, req) + return getResult{ + status: rec.Code, + clen: rec.Header().Get(xhttp.ContentLength), + crange: rec.Header().Get(xhttp.ContentRange), + } + } + + queries := []string{"?partNumber=1", "?partNumber=2", "?partNumber=3"} + srcGets := make([]getResult, len(queries)) + for i, q := range queries { + srcGets[i] = doGet(q, nil) + t.Logf("[%s] SOURCE GET %s -> status=%d Content-Length=%s Content-Range=%s len(body)=%d", + instanceType, q, srcGets[i].status, srcGets[i].clen, srcGets[i].crange, len(srcGets[i].body)) + } + // Range GET crossing the part1/part2 boundary. + boundaryRange := fmt.Sprintf("bytes=%d-%d", 5*mib-16, 5*mib+15) + srcRange := doGet("", map[string]string{"Range": boundaryRange}) + t.Logf("[%s] SOURCE GET Range %s -> status=%d Content-Length=%s len(body)=%d", + instanceType, boundaryRange, srcRange.status, srcRange.clen, len(srcRange.body)) + + // Sanity: the source must return exactly the part bytes. + for i := range partData { + if !bytes.Equal(srcGets[i].body, partData[i]) { + t.Fatalf("[%s] SOURCE partNumber=%d returned wrong bytes (len %d want %d)", + instanceType, i+1, len(srcGets[i].body), len(partData[i])) + } + } + + // ---- 3. Read the raw ciphertext the replication worker would ship. ---- + gr, err := obj.GetObjectNInfo(t.Context(), bucketName, object, nil, http.Header{}, ObjectOptions{ReplicationRequest: true}) + if err != nil { + t.Fatal(err) + } + sourceInfo := gr.ObjInfo + rawAll, err := io.ReadAll(gr) + gr.Close() + if err != nil { + t.Fatal(err) + } + if bytes.Equal(rawAll, plaintext) { + t.Fatal("source replication read did not return encrypted bytes") + } + rawParts := make([][]byte, len(sourceInfo.Parts)) + off := int64(0) + for i, p := range sourceInfo.Parts { + rawParts[i] = rawAll[off : off+p.Size] + off += p.Size + } + if off != int64(len(rawAll)) { + t.Fatalf("raw ciphertext length %d != sum of part sizes %d", len(rawAll), off) + } + + // ---- 4. Replicate onto the same key through the trusted write path. ---- + replicationOpts, isMP, err := putReplicationOpts(t.Context(), "", sourceInfo) + if err != nil { + t.Fatal(err) + } + if !isMP { + t.Fatal("SSE-C multipart source was not recognized as multipart") + } + replicationOpts.Internal.SourceMTime = time.Time{} + replicationHeaders := make(map[string]string) + for name, values := range replicationOpts.Header() { + if len(values) > 0 { + replicationHeaders[name] = values[0] + } + } + replNewReq, err := newTestSignedRequestV4(http.MethodPost, getNewMultipartURL("", bucketName, object), + 0, nil, replicator.AccessKey, replicator.SecretKey, replicationHeaders) + if err != nil { + t.Fatal(err) + } + replNewRec := httptest.NewRecorder() + apiRouter.ServeHTTP(replNewRec, replNewReq) + if replNewRec.Code != http.StatusOK { + t.Fatalf("replica NewMultipart status %d: %s", replNewRec.Code, replNewRec.Body.String()) + } + var replInit InitiateMultipartUploadResponse + if err = xmlDecoder(replNewRec.Body, &replInit, int64(replNewRec.Body.Len())); err != nil { + t.Fatal(err) + } + + replParts := make([]CompletePart, len(rawParts)) + for i, raw := range rawParts { + pn := strconv.Itoa(i + 1) + req, err := newTestSignedRequestV4(http.MethodPut, + getPutObjectPartURL("", bucketName, object, replInit.UploadID, pn), + int64(len(raw)), bytes.NewReader(raw), replicator.AccessKey, replicator.SecretKey, + map[string]string{xhttp.MinIOSourceReplicationRequest: "true"}) + if err != nil { + t.Fatal(err) + } + rec := httptest.NewRecorder() + apiRouter.ServeHTTP(rec, req) + if rec.Code != http.StatusOK { + t.Fatalf("replica PutPart %s status %d: %s", pn, rec.Code, rec.Body.String()) + } + replParts[i] = CompletePart{PartNumber: i + 1, ETag: canonicalizeETag(rec.Header()[xhttp.ETag][0])} + } + replCompleteBody, err := xml.Marshal(CompleteMultipartUpload{Parts: replParts}) + if err != nil { + t.Fatal(err) + } + srcActualSize, err := sourceInfo.GetActualSize() + if err != nil { + t.Fatal(err) + } + replCompleteHeaders := map[string]string{ + xhttp.MinIOSourceReplicationRequest: "true", + xhttp.MinIOSourceMTime: sourceInfo.ModTime.Format(time.RFC3339Nano), + xhttp.MinIOSourceETag: sourceInfo.ETag, + xhttp.MinIOReplicationActualObjectSize: strconv.FormatInt(srcActualSize, 10), + } + replCompleteReq, err := newTestSignedRequestV4(http.MethodPost, + getCompleteMultipartUploadURL("", bucketName, object, replInit.UploadID), int64(len(replCompleteBody)), + bytes.NewReader(replCompleteBody), replicator.AccessKey, replicator.SecretKey, replCompleteHeaders) + if err != nil { + t.Fatal(err) + } + replCompleteRec := httptest.NewRecorder() + apiRouter.ServeHTTP(replCompleteRec, replCompleteReq) + if replCompleteRec.Code != http.StatusOK { + t.Fatalf("replica Complete status %d: %s", replCompleteRec.Code, replCompleteRec.Body.String()) + } + + // ---- 5. Same reads against the replica. ---- + repOI, err := obj.GetObjectInfo(t.Context(), bucketName, object, ObjectOptions{}) + if err != nil { + t.Fatal(err) + } + t.Logf("[%s] REPLICA parts:", instanceType) + for _, p := range repOI.Parts { + t.Logf(" part %d Size=%d ActualSize=%d", p.Number, p.Size, p.ActualSize) + } + repActual, err := repOI.GetActualSize() + if err != nil { + t.Fatal(err) + } + t.Logf("[%s] REPLICA object Size=%d GetActualSize=%d actual-size-meta=%q", + instanceType, repOI.Size, repActual, repOI.UserDefined[ReservedMetadataPrefix+"actual-size"]) + + // Whole-object GET must still be byte-identical. + whole := doGet("", nil) + if whole.status != http.StatusOK || !bytes.Equal(whole.body, plaintext) { + t.Errorf("[%s] REPLICA whole-object GET: status=%d len=%d want %d, equal=%v", + instanceType, whole.status, len(whole.body), len(plaintext), bytes.Equal(whole.body, plaintext)) + } else { + t.Logf("[%s] REPLICA whole-object GET: OK, %d bytes identical", instanceType, len(whole.body)) + } + + // Fresh replica parts must record the plaintext lengths, not the + // ciphertext lengths the sender shipped. + if len(repOI.Parts) != len(partLens) { + t.Fatalf("[%s] REPLICA has %d parts, want %d", instanceType, len(repOI.Parts), len(partLens)) + } + for i, p := range repOI.Parts { + if p.ActualSize != int64(partLens[i]) { + t.Errorf("[%s] REPLICA part %d ActualSize=%d, want the uploaded length %d", + instanceType, p.Number, p.ActualSize, partLens[i]) + } + } + + // Expected framing from independent prefix sums of the uploaded lengths. + total := len(plaintext) + start := 0 + for i, q := range queries { + wantLen := partLens[i] + wantRange := fmt.Sprintf("bytes %d-%d/%d", start, start+wantLen-1, total) + start += wantLen + + got := doGet(q, nil) + want := srcGets[i] + if got.status != want.status || got.status != http.StatusPartialContent { + t.Errorf("[%s] REPLICA GET %s status=%d, source=%d, want 206", instanceType, q, got.status, want.status) + } + if got.clen != strconv.Itoa(wantLen) || got.crange != wantRange { + t.Errorf("[%s] REPLICA GET %s Content-Length=%s Content-Range=%s, want %d and %q", + instanceType, q, got.clen, got.crange, wantLen, wantRange) + } + if want.clen != strconv.Itoa(wantLen) || want.crange != wantRange { + t.Errorf("[%s] SOURCE GET %s Content-Length=%s Content-Range=%s, want %d and %q", + instanceType, q, want.clen, want.crange, wantLen, wantRange) + } + if len(got.body) != wantLen { + t.Errorf("[%s] REPLICA GET %s body is %d bytes, want %d", instanceType, q, len(got.body), wantLen) + } + if !bytes.Equal(got.body, want.body) { + firstDiff := -1 + for k := 0; k < len(got.body) && k < len(want.body); k++ { + if got.body[k] != want.body[k] { + firstDiff = k + break + } + } + t.Errorf("[%s] REPLICA partNumber=%d returned DIFFERENT bytes than the source: got %d bytes (Content-Range %q), want %d bytes (Content-Range %q), first differing byte at %d", + instanceType, i+1, len(got.body), got.crange, len(want.body), want.crange, firstDiff) + } + + head := doHead(q) + if head.status != http.StatusPartialContent || head.clen != strconv.Itoa(wantLen) || head.crange != wantRange { + t.Errorf("[%s] REPLICA HEAD %s status=%d Content-Length=%s Content-Range=%s, want 206, %d and %q", + instanceType, q, head.status, head.clen, head.crange, wantLen, wantRange) + } + } + + repRange := doGet("", map[string]string{"Range": boundaryRange}) + sameRange := bytes.Equal(repRange.body, srcRange.body) + t.Logf("[%s] REPLICA GET Range %s -> status=%d Content-Length=%s len(body)=%d | source len=%d | bytes-equal=%v", + instanceType, boundaryRange, repRange.status, repRange.clen, len(repRange.body), len(srcRange.body), sameRange) + if !sameRange { + t.Errorf("[%s] REPLICA boundary Range GET returned different bytes", instanceType) + } +} + +// TestSSECReplicaPartActualSizeDataMovement reproduces what a decommission or +// rebalance does to a replica whose parts already carry the ciphertext length in +// ActualSize: it replays the object through the object layer exactly the way +// decommissionObject does (cmd/erasure-server-pool-decom.go:605-667), passing the +// stale ActualSize to PutObjectPart and completing without ReplicationRequest, so +// CompleteMultipartUpload recomputes the object-level actual-size from the sum of +// part ActualSizes (cmd/erasure-multipart.go:1365,1440). +func TestSSECReplicaPartActualSizeDataMovement(t *testing.T) { + defer DetectTestLeak(t)() + ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{ + t: t, + objAPITest: testSSECReplicaPartActualSizeDataMovement, + }) +} + +func testSSECReplicaPartActualSizeDataMovement(obj ObjectLayer, instanceType, bucketName string, + apiRouter http.Handler, credentials auth.Credentials, t *testing.T, +) { + previousTLS := globalIsTLS + globalIsTLS = true + defer func() { globalIsTLS = previousTLS }() + + key := bytes.Repeat([]byte{0x37}, 32) + keyMD5 := md5.Sum(key) + sseHeaders := map[string]string{ + xhttp.AmzServerSideEncryptionCustomerAlgorithm: xhttp.AmzEncryptionAES, + xhttp.AmzServerSideEncryptionCustomerKey: base64.StdEncoding.EncodeToString(key), + xhttp.AmzServerSideEncryptionCustomerKeyMD5: base64.StdEncoding.EncodeToString(keyMD5[:]), + } + + const mib = 1024 * 1024 + partLens := []int{5 * mib, 5 * mib, 1 * mib} + plaintext := make([]byte, 0, 11*mib) + partData := make([][]byte, len(partLens)) + for i, n := range partLens { + b := make([]byte, n) + for j := range b { + b[j] = byte(i*13 + j%241) + } + partData[i] = b + plaintext = append(plaintext, b...) + } + object := "ssec-mp-datamovement" + + newRec := httptest.NewRecorder() + newReq, err := newTestSignedRequestV4(http.MethodPost, getNewMultipartURL("", bucketName, object), + 0, nil, credentials.AccessKey, credentials.SecretKey, sseHeaders) + if err != nil { + t.Fatal(err) + } + apiRouter.ServeHTTP(newRec, newReq) + if newRec.Code != http.StatusOK { + t.Fatalf("NewMultipart status %d: %s", newRec.Code, newRec.Body.String()) + } + var init InitiateMultipartUploadResponse + if err = xmlDecoder(newRec.Body, &init, int64(newRec.Body.Len())); err != nil { + t.Fatal(err) + } + srcParts := make([]CompletePart, len(partLens)) + for i, b := range partData { + req, err := newTestSignedRequestV4(http.MethodPut, + getPutObjectPartURL("", bucketName, object, init.UploadID, strconv.Itoa(i+1)), + int64(len(b)), bytes.NewReader(b), credentials.AccessKey, credentials.SecretKey, sseHeaders) + if err != nil { + t.Fatal(err) + } + rec := httptest.NewRecorder() + apiRouter.ServeHTTP(rec, req) + if rec.Code != http.StatusOK { + t.Fatalf("PutPart %d status %d: %s", i+1, rec.Code, rec.Body.String()) + } + srcParts[i] = CompletePart{PartNumber: i + 1, ETag: canonicalizeETag(rec.Header()[xhttp.ETag][0])} + } + body, err := xml.Marshal(CompleteMultipartUpload{Parts: srcParts}) + if err != nil { + t.Fatal(err) + } + cReq, err := newTestSignedRequestV4(http.MethodPost, + getCompleteMultipartUploadURL("", bucketName, object, init.UploadID), int64(len(body)), + bytes.NewReader(body), credentials.AccessKey, credentials.SecretKey, sseHeaders) + if err != nil { + t.Fatal(err) + } + cRec := httptest.NewRecorder() + apiRouter.ServeHTTP(cRec, cReq) + if cRec.Code != http.StatusOK { + t.Fatalf("Complete status %d: %s", cRec.Code, cRec.Body.String()) + } + + // Replay it the way decommissionObject does, but hand PutObjectPart the STALE + // ActualSize an already-written SSE-C replica carries: the ciphertext length. + gr, err := obj.GetObjectNInfo(t.Context(), bucketName, object, nil, http.Header{}, + ObjectOptions{NoDecryption: true, NoLock: true, NoAuditLog: true}) + if err != nil { + t.Fatal(err) + } + oi := gr.ObjInfo + res, err := obj.NewMultipartUpload(t.Context(), bucketName, object, ObjectOptions{ + UserDefined: oi.UserDefined, + DataMovement: true, + NoAuditLog: true, + }) + if err != nil { + gr.Close() + t.Fatal(err) + } + moved := make([]CompletePart, len(oi.Parts)) + for i, part := range oi.Parts { + staleActual := part.Size // what a bad replica records + hr, herr := hash.NewReader(t.Context(), io.LimitReader(gr, part.Size), part.Size, "", "", staleActual) + if herr != nil { + gr.Close() + t.Fatal(herr) + } + pi, perr := obj.PutObjectPart(t.Context(), bucketName, object, res.UploadID, part.Number, + NewPutObjReader(hr), ObjectOptions{ + PreserveETag: part.ETag, + IndexCB: func() []byte { return part.Index }, + NoAuditLog: true, + }) + if perr != nil { + gr.Close() + t.Fatalf("data-movement PutObjectPart part %d: %v", part.Number, perr) + } + moved[i] = CompletePart{ETag: pi.ETag, PartNumber: pi.PartNumber} + } + gr.Close() + + // decommissionObject/rebalanceObject complete WITHOUT ReplicationRequest, so + // the object-level actual-size is recomputed from the part ActualSizes. + if _, err = obj.CompleteMultipartUpload(t.Context(), bucketName, object, res.UploadID, moved, + ObjectOptions{DataMovement: true, MTime: oi.ModTime, NoAuditLog: true}); err != nil { + t.Fatalf("data-movement CompleteMultipartUpload: %v", err) + } + + after, err := obj.GetObjectInfo(t.Context(), bucketName, object, ObjectOptions{}) + if err != nil { + t.Fatal(err) + } + t.Logf("[%s] AFTER DATA MOVEMENT parts:", instanceType) + for _, p := range after.Parts { + t.Logf(" part %d Size=%d ActualSize=%d", p.Number, p.Size, p.ActualSize) + } + gotActual, err := after.GetActualSize() + if err != nil { + t.Fatalf("GetActualSize after data movement: %v", err) + } + t.Logf("[%s] AFTER DATA MOVEMENT object Size=%d GetActualSize=%d actual-size-meta=%q", + instanceType, after.Size, gotActual, after.UserDefined[ReservedMetadataPrefix+"actual-size"]) + + wantActual := int64(len(plaintext)) + if gotActual != wantActual { + t.Errorf("[%s] object-level actual size after data movement = %d, want %d", + instanceType, gotActual, wantActual) + } + for i, p := range after.Parts { + if p.ActualSize != int64(partLens[i]) { + t.Errorf("[%s] part %d ActualSize after data movement = %d, want %d", + instanceType, p.Number, p.ActualSize, partLens[i]) + } + } + + getReq, err := newTestSignedRequestV4(http.MethodGet, getGetObjectURL("", bucketName, object), + 0, nil, credentials.AccessKey, credentials.SecretKey, sseHeaders) + if err != nil { + t.Fatal(err) + } + getRec := httptest.NewRecorder() + apiRouter.ServeHTTP(getRec, getReq) + if getRec.Code != http.StatusOK || !bytes.Equal(getRec.Body.Bytes(), plaintext) { + t.Errorf("[%s] whole-object GET after data movement: status=%d len=%d want %d equal=%v", + instanceType, getRec.Code, getRec.Body.Len(), len(plaintext), bytes.Equal(getRec.Body.Bytes(), plaintext)) + } + // The advertised length must be the body length: a poisoned object-level + // actual-size shows up here as a Content-Length larger than the body. + if clen := getRec.Header().Get(xhttp.ContentLength); clen != strconv.Itoa(len(plaintext)) || clen != strconv.Itoa(getRec.Body.Len()) { + t.Errorf("[%s] whole-object GET after data movement advertises Content-Length=%s for a %d-byte body (plaintext %d)", + instanceType, clen, getRec.Body.Len(), len(plaintext)) + } +} + +// TestPartNumberToRangeSpecEncryptedParts pins the read-side repair: for an +// encrypted, uncompressed object the part range is derived from the stored +// ciphertext length, so a replica whose parts still record the ciphertext +// length in ActualSize reads correctly, while plaintext and compressed objects +// keep using ActualSize, and a part whose length cannot be a valid encrypted +// stream is reported as tampered by both callers. See pgsty/silo#119. +func TestPartNumberToRangeSpecEncryptedParts(t *testing.T) { + const mib = 1024 * 1024 + plain := []int64{5 * mib, 5 * mib, 1024} + cipher := make([]int64, len(plain)) + for i, n := range plain { + c, err := sio.EncryptedSize(uint64(n)) + if err != nil { + t.Fatal(err) + } + cipher[i] = int64(c) + } + sum := func(v []int64) (s int64) { + for _, n := range v { + s += n + } + return s + } + encMeta := map[string]string{ + crypto.MetaSealedKeySSEC: "sealed-key", + crypto.MetaIV: "iv", + crypto.MetaAlgorithm: crypto.InsecureSealAlgorithm, + } + compressedEncMeta := map[string]string{ReservedMetadataPrefix + "compression": compressionAlgorithmV2} + for k, v := range encMeta { + compressedEncMeta[k] = v + } + mkParts := func(sizes, actual []int64) []ObjectPartInfo { + parts := make([]ObjectPartInfo, len(sizes)) + for i := range sizes { + parts[i] = ObjectPartInfo{Number: i + 1, Size: sizes[i], ActualSize: actual[i]} + } + return parts + } + + // A compressed part's ActualSize is the uploaded length before compression, + // which bears no relation to the ciphertext length: use lengths whose + // decrypted size differs from ActualSize so that dropping the compression + // exclusion is detectable. + uploaded := []int64{2 * plain[0], 2 * plain[1], 2 * plain[2]} + wantRangeOf := func(lens []int64, pn int) (start, end int64) { + for i := 0; i < pn-1; i++ { + start += lens[i] + } + return start, start + lens[pn-1] - 1 + } + for _, tc := range []struct { + name string + oi ObjectInfo + lens []int64 + }{ + {"encrypted, stale ciphertext ActualSize", ObjectInfo{Size: sum(cipher), UserDefined: encMeta, Parts: mkParts(cipher, cipher)}, plain}, + {"encrypted, correct ActualSize", ObjectInfo{Size: sum(cipher), UserDefined: encMeta, Parts: mkParts(cipher, plain)}, plain}, + {"plaintext", ObjectInfo{Size: sum(plain), UserDefined: map[string]string{}, Parts: mkParts(plain, plain)}, plain}, + {"compressed and encrypted keeps ActualSize", ObjectInfo{Size: sum(cipher), UserDefined: compressedEncMeta, Parts: mkParts(cipher, uploaded)}, uploaded}, + } { + for pn := 1; pn <= len(plain); pn++ { + rs, err := partNumberToRangeSpec(tc.oi, pn) + if err != nil { + t.Fatalf("%s: partNumber=%d: %v", tc.name, pn, err) + } + start, end := wantRangeOf(tc.lens, pn) + if rs == nil || rs.Start != start || rs.End != end { + t.Errorf("%s: partNumber=%d range %+v, want %d-%d", tc.name, pn, rs, start, end) + } + } + } + + // A 31-byte part cannot be a sio stream: both callers report it as tampered. + bad := ObjectInfo{ + Size: 31 + cipher[1] + cipher[2], + UserDefined: encMeta, + Parts: mkParts([]int64{31, cipher[1], cipher[2]}, []int64{31, plain[1], plain[2]}), + } + for pn := 1; pn <= 2; pn++ { + if _, err := partNumberToRangeSpec(bad, pn); err != errObjectTampered { + t.Errorf("malformed part: partNumber=%d err=%v, want errObjectTampered", pn, err) + } + } + if _, _, _, err := NewGetObjectReader(nil, bad, ObjectOptions{PartNumber: 1}, http.Header{}); err != errObjectTampered { + t.Errorf("NewGetObjectReader on a malformed part: err=%v, want errObjectTampered", err) + } + if err := setObjectHeaders(t.Context(), httptest.NewRecorder(), bad, nil, ObjectOptions{PartNumber: 1}); err != errObjectTampered { + t.Errorf("setObjectHeaders on a malformed part: err=%v, want errObjectTampered", err) + } + + // A zero-length trailing part is a valid (empty) stream and stays accepted. + zero := ObjectInfo{Size: cipher[0], UserDefined: encMeta, Parts: mkParts([]int64{cipher[0], 0}, []int64{cipher[0], 0})} + rs, err := partNumberToRangeSpec(zero, 2) + if err != nil || rs == nil || rs.Start != plain[0] { + t.Errorf("zero-length trailing part: range %+v err=%v, want start %d", rs, err, plain[0]) + } +} + +// TestAPISSECReplicaMalformedPartIsRejected asserts that a trusted SSE-C replica +// part whose ciphertext length cannot be a valid encrypted stream is rejected +// as tampered before the part is committed. See pgsty/silo#119. +func TestAPISSECReplicaMalformedPartIsRejected(t *testing.T) { + defer DetectTestLeak(t)() + ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{t: t, objAPITest: testAPISSECReplicaMalformedPartIsRejected}) +} + +func testAPISSECReplicaMalformedPartIsRejected(obj ObjectLayer, instanceType, bucketName string, + apiRouter http.Handler, credentials auth.Credentials, t *testing.T, +) { + previousTLS := globalIsTLS + globalIsTLS = true + defer func() { globalIsTLS = previousTLS }() + + replicator := newObjectAttributesAuthzUser(t, instanceType, bucketName, `"s3:PutObject","s3:GetObject","s3:ReplicateObject"`) + key := bytes.Repeat([]byte{0x45}, 32) + keyMD5 := md5.Sum(key) + sseHeaders := map[string]string{ + xhttp.AmzServerSideEncryptionCustomerAlgorithm: xhttp.AmzEncryptionAES, + xhttp.AmzServerSideEncryptionCustomerKey: base64.StdEncoding.EncodeToString(key), + xhttp.AmzServerSideEncryptionCustomerKeyMD5: base64.StdEncoding.EncodeToString(keyMD5[:]), + } + object := "ssec-replica-malformed" + data := bytes.Repeat([]byte("malformed-part-"), 1024) + + putReq, err := newTestSignedRequestV4(http.MethodPut, getPutObjectURL("", bucketName, object), int64(len(data)), + bytes.NewReader(data), credentials.AccessKey, credentials.SecretKey, sseHeaders) + if err != nil { + t.Fatal(err) + } + putRec := httptest.NewRecorder() + apiRouter.ServeHTTP(putRec, putReq) + if putRec.Code != http.StatusOK { + t.Fatalf("%s: source PUT %d: %s", instanceType, putRec.Code, putRec.Body.String()) + } + gr, err := obj.GetObjectNInfo(t.Context(), bucketName, object, nil, http.Header{}, ObjectOptions{ReplicationRequest: true}) + if err != nil { + t.Fatal(err) + } + sourceInfo := gr.ObjInfo + raw, err := io.ReadAll(gr) + gr.Close() + if err != nil { + t.Fatal(err) + } + replicationOpts, _, err := putReplicationOpts(t.Context(), "", sourceInfo) + if err != nil { + t.Fatal(err) + } + replicationOpts.Internal.SourceMTime = time.Time{} + replicationHeaders := make(map[string]string) + for name, values := range replicationOpts.Header() { + if len(values) > 0 { + replicationHeaders[name] = values[0] + } + } + newReq, err := newTestSignedRequestV4(http.MethodPost, getNewMultipartURL("", bucketName, object), + 0, nil, replicator.AccessKey, replicator.SecretKey, replicationHeaders) + if err != nil { + t.Fatal(err) + } + newRec := httptest.NewRecorder() + apiRouter.ServeHTTP(newRec, newReq) + if newRec.Code != http.StatusOK { + t.Fatalf("%s: replica NewMultipart %d: %s", instanceType, newRec.Code, newRec.Body.String()) + } + var init InitiateMultipartUploadResponse + if err = xmlDecoder(newRec.Body, &init, int64(newRec.Body.Len())); err != nil { + t.Fatal(err) + } + partHeaders := map[string]string{xhttp.MinIOSourceReplicationRequest: "true"} + + // 31 bytes cannot be a sio stream (the package header plus its + // authentication tag occupy 32 bytes). + badReq, err := newTestSignedRequestV4(http.MethodPut, getPutObjectPartURL("", bucketName, object, init.UploadID, "1"), + 31, bytes.NewReader(raw[:31]), replicator.AccessKey, replicator.SecretKey, partHeaders) + if err != nil { + t.Fatal(err) + } + badRec := httptest.NewRecorder() + apiRouter.ServeHTTP(badRec, badReq) + if badRec.Code == http.StatusOK || !strings.Contains(badRec.Body.String(), "XMinioObjectTampered") { + t.Fatalf("%s: malformed replica part answered %d: %s", instanceType, badRec.Code, badRec.Body.String()) + } + lpi, err := obj.ListObjectParts(t.Context(), bucketName, object, init.UploadID, 0, 10, ObjectOptions{}) + if err != nil { + t.Fatal(err) + } + if len(lpi.Parts) != 0 { + t.Fatalf("%s: malformed replica part was committed: %+v", instanceType, lpi.Parts) + } + + // Control: the real ciphertext still uploads on the same upload. + goodReq, err := newTestSignedRequestV4(http.MethodPut, getPutObjectPartURL("", bucketName, object, init.UploadID, "1"), + int64(len(raw)), bytes.NewReader(raw), replicator.AccessKey, replicator.SecretKey, partHeaders) + if err != nil { + t.Fatal(err) + } + goodRec := httptest.NewRecorder() + apiRouter.ServeHTTP(goodRec, goodReq) + if goodRec.Code != http.StatusOK { + t.Fatalf("%s: valid replica part answered %d: %s", instanceType, goodRec.Code, goodRec.Body.String()) + } + lpi, err = obj.ListObjectParts(t.Context(), bucketName, object, init.UploadID, 0, 10, ObjectOptions{}) + if err != nil { + t.Fatal(err) + } + if len(lpi.Parts) != 1 || lpi.Parts[0].ActualSize != int64(len(data)) { + t.Fatalf("%s: valid replica part recorded %+v, want one part with ActualSize %d", instanceType, lpi.Parts, len(data)) + } +} diff --git a/cmd/erasure-multipart-upload-checksum_test.go b/cmd/erasure-multipart-upload-checksum_test.go new file mode 100644 index 000000000..dd10a2026 --- /dev/null +++ b/cmd/erasure-multipart-upload-checksum_test.go @@ -0,0 +1,596 @@ +// Copyright (c) 2015-2026 MinIO, Inc. +// +// This file is part of MinIO Object Storage stack +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package cmd + +import ( + "bytes" + "crypto/md5" + "encoding/base64" + "encoding/xml" + "net/http" + "net/http/httptest" + "strconv" + "testing" + + "github.com/minio/minio/internal/auth" + "github.com/minio/minio/internal/hash" + xhttp "github.com/minio/minio/internal/http" + "github.com/minio/minio/internal/kms" +) + +func uploadPartHTTP(t *testing.T, apiRouter http.Handler, creds auth.Credentials, + bucket, object, uploadID string, partNumber int, data []byte, headers map[string]string, +) (string, *httptest.ResponseRecorder) { + t.Helper() + req, err := newTestSignedRequestV4(http.MethodPut, + getPutObjectPartURL("", bucket, object, uploadID, strconv.Itoa(partNumber)), + int64(len(data)), bytes.NewReader(data), creds.AccessKey, creds.SecretKey, headers) + if err != nil { + t.Fatalf("failed to build UploadPart request: %v", err) + } + rec := httptest.NewRecorder() + apiRouter.ServeHTTP(rec, req) + if rec.Code != http.StatusOK { + t.Fatalf("UploadPart failed: %d %s", rec.Code, rec.Body.String()) + } + return canonicalizeETag(rec.Header()[xhttp.ETag][0]), rec +} + +func listPartsHTTP(t *testing.T, apiRouter http.Handler, creds auth.Credentials, + bucket, object, uploadID string, headers map[string]string, +) ListPartsResponse { + t.Helper() + req, err := newTestSignedRequestV4(http.MethodGet, + getListMultipartURLWithParams("", bucket, object, uploadID, "1000", "", ""), + 0, nil, creds.AccessKey, creds.SecretKey, headers) + if err != nil { + t.Fatalf("failed to build ListParts request: %v", err) + } + rec := httptest.NewRecorder() + apiRouter.ServeHTTP(rec, req) + if rec.Code != http.StatusOK { + t.Fatalf("ListParts failed: %d %s", rec.Code, rec.Body.String()) + } + var response ListPartsResponse + if err := xml.Unmarshal(rec.Body.Bytes(), &response); err != nil { + t.Fatalf("failed to decode ListParts response: %v", err) + } + return response +} + +func partChecksum(typ hash.ChecksumType, part Part) string { + switch typ.Base() { + case hash.ChecksumCRC32: + return part.ChecksumCRC32 + case hash.ChecksumCRC32C: + return part.ChecksumCRC32C + case hash.ChecksumSHA1: + return part.ChecksumSHA1 + case hash.ChecksumSHA256: + return part.ChecksumSHA256 + case hash.ChecksumCRC64NVME: + return part.ChecksumCRC64NVME + default: + return "" + } +} + +func copyPartChecksum(typ hash.ChecksumType, response CopyObjectPartResponse) string { + switch typ.Base() { + case hash.ChecksumCRC32: + return response.ChecksumCRC32 + case hash.ChecksumCRC32C: + return response.ChecksumCRC32C + case hash.ChecksumSHA1: + return response.ChecksumSHA1 + case hash.ChecksumSHA256: + return response.ChecksumSHA256 + case hash.ChecksumCRC64NVME: + return response.ChecksumCRC64NVME + default: + return "" + } +} + +func completePartWithChecksum(typ hash.ChecksumType, partNumber int, etag, checksum string) CompletePart { + part := CompletePart{PartNumber: partNumber, ETag: etag} + switch typ.Base() { + case hash.ChecksumCRC32: + part.ChecksumCRC32 = checksum + case hash.ChecksumCRC32C: + part.ChecksumCRC32C = checksum + case hash.ChecksumSHA1: + part.ChecksumSHA1 = checksum + case hash.ChecksumSHA256: + part.ChecksumSHA256 = checksum + case hash.ChecksumCRC64NVME: + part.ChecksumCRC64NVME = checksum + } + return part +} + +func completePartsHTTP(t *testing.T, apiRouter http.Handler, creds auth.Credentials, + bucket, object, uploadID string, parts []CompletePart, headers map[string]string, +) *httptest.ResponseRecorder { + t.Helper() + body, err := xml.Marshal(CompleteMultipartUpload{Parts: parts}) + if err != nil { + t.Fatalf("failed to encode CompleteMultipartUpload request: %v", err) + } + req, err := newTestSignedRequestV4(http.MethodPost, + getCompleteMultipartUploadURL("", bucket, object, uploadID), + int64(len(body)), bytes.NewReader(body), creds.AccessKey, creds.SecretKey, headers) + if err != nil { + t.Fatalf("failed to build CompleteMultipartUpload request: %v", err) + } + rec := httptest.NewRecorder() + apiRouter.ServeHTTP(rec, req) + return rec +} + +func copyPartWithoutChecksumHTTP(t *testing.T, apiRouter http.Handler, creds auth.Credentials, + bucket, source, object, uploadID, sourceRange string, headers map[string]string, +) CopyObjectPartResponse { + t.Helper() + req, err := newTestSignedRequestV4(http.MethodPut, + getCopyObjectPartURL("", bucket, object, uploadID, "1"), + 0, nil, creds.AccessKey, creds.SecretKey, headers) + if err != nil { + t.Fatalf("failed to build UploadPartCopy request: %v", err) + } + req.Header.Set(xhttp.AmzCopySource, SlashSeparator+pathJoin(bucket, source)) + if sourceRange != "" { + req.Header.Set(xhttp.AmzCopySourceRange, sourceRange) + } + rec := httptest.NewRecorder() + apiRouter.ServeHTTP(rec, req) + if rec.Code != http.StatusOK { + t.Fatalf("UploadPartCopy failed: %d %s", rec.Code, rec.Body.String()) + } + var response CopyObjectPartResponse + if err := xml.Unmarshal(rec.Body.Bytes(), &response); err != nil { + t.Fatalf("failed to decode UploadPartCopy response: %v", err) + } + return response +} + +// TestAPIUploadPartServerSideChecksum exercises the data transformations that +// made installing a checksum hasher in the object layer unsafe. The checksum +// must always cover logical plaintext, regardless of compression or encryption. +func TestAPIUploadPartServerSideChecksum(t *testing.T) { + defer DetectTestLeak(t)() + ExecExtendedObjectLayerAPITest(t, testAPIUploadPartServerSideChecksum, + []string{"CopyObjectPart", "PutObjectPart", "NewMultipart", "ListObjectParts", "CompleteMultipart"}) +} + +func testAPIUploadPartServerSideChecksum(obj ObjectLayer, instanceType, bucketName string, apiRouter http.Handler, + credentials auth.Credentials, t *testing.T, +) { + typ := hash.ChecksumCRC32 + data := bytes.Repeat([]byte("multipart-checksum-plaintext-"), 48*1024) + want := mustChecksum(t, typ, data) + + t.Run("upload", func(t *testing.T) { + object := "checksums/upload" + uploadID := newMultipartUploadHTTP(t, apiRouter, credentials, bucketName, object, + typ.String(), xhttp.AmzChecksumTypeFullObject) + etag, rec := uploadPartHTTP(t, apiRouter, credentials, + bucketName, object, uploadID, 1, data, nil) + if got := rec.Header().Get(typ.Key()); got != "" { + t.Fatalf("%s: UploadPart returned server-computed checksum %q", instanceType, got) + } + + listed := listPartsHTTP(t, apiRouter, credentials, bucketName, object, uploadID, nil) + if len(listed.Parts) != 1 || partChecksum(typ, listed.Parts[0]) != want { + t.Fatalf("%s: ListParts checksum mismatch: %+v, want %q", instanceType, listed.Parts, want) + } + + rec = completePartsHTTP(t, apiRouter, credentials, bucketName, object, uploadID, + []CompletePart{{PartNumber: 1, ETag: etag}}, nil) + if rec.Code != http.StatusOK { + t.Fatalf("%s: CompleteMultipartUpload failed: %d %s", instanceType, rec.Code, rec.Body.String()) + } + + oi, err := obj.GetObjectInfo(t.Context(), bucketName, object, ObjectOptions{}) + if err != nil { + t.Fatalf("%s: GetObjectInfo failed: %v", instanceType, err) + } + checksums, _ := oi.decryptChecksums(0, nil) + if got := checksums[typ.String()]; got != want { + t.Fatalf("%s: stored checksum %q, want plaintext checksum %q", instanceType, got, want) + } + }) + + t.Run("copy", func(t *testing.T) { + source := "checksums/source" + if _, err := obj.PutObject(t.Context(), bucketName, source, + mustGetPutObjReader(t, bytes.NewReader(data), int64(len(data)), "", ""), ObjectOptions{}); err != nil { + t.Fatalf("%s: source PutObject failed: %v", instanceType, err) + } + object := "checksums/copy" + uploadID := newMultipartUploadHTTP(t, apiRouter, credentials, bucketName, object, + typ.String(), xhttp.AmzChecksumTypeFullObject) + response := copyPartWithoutChecksumHTTP(t, apiRouter, credentials, + bucketName, source, object, uploadID, "", nil) + if got := copyPartChecksum(typ, response); got != want { + t.Fatalf("%s: UploadPartCopy checksum %q, want %q", instanceType, got, want) + } + + listed := listPartsHTTP(t, apiRouter, credentials, bucketName, object, uploadID, nil) + if len(listed.Parts) != 1 || partChecksum(typ, listed.Parts[0]) != want { + t.Fatalf("%s: copied ListParts checksum mismatch: %+v, want %q", instanceType, listed.Parts, want) + } + + rec := completePartsHTTP(t, apiRouter, credentials, bucketName, object, uploadID, + []CompletePart{{PartNumber: 1, ETag: canonicalizeETag(response.ETag)}}, nil) + if rec.Code != http.StatusOK { + t.Fatalf("%s: copied CompleteMultipartUpload failed: %d %s", instanceType, rec.Code, rec.Body.String()) + } + }) +} + +func TestAPIUploadPartServerSideChecksumAlgorithms(t *testing.T) { + defer DetectTestLeak(t)() + ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{ + t: t, + objAPITest: testAPIUploadPartServerSideChecksumAlgorithms, + endpoints: []string{"CopyObjectPart", "PutObjectPart", "NewMultipart", "ListObjectParts", "CompleteMultipart"}, + }) +} + +func testAPIUploadPartServerSideChecksumAlgorithms(obj ObjectLayer, instanceType, bucketName string, apiRouter http.Handler, + credentials auth.Credentials, t *testing.T, +) { + tests := []struct { + typ hash.ChecksumType + objType string + composite bool + }{ + {hash.ChecksumCRC32, xhttp.AmzChecksumTypeFullObject, false}, + {hash.ChecksumCRC32C, xhttp.AmzChecksumTypeFullObject, false}, + {hash.ChecksumCRC64NVME, xhttp.AmzChecksumTypeFullObject, false}, + {hash.ChecksumCRC32, xhttp.AmzChecksumTypeComposite, true}, + {hash.ChecksumSHA1, xhttp.AmzChecksumTypeComposite, true}, + {hash.ChecksumSHA256, xhttp.AmzChecksumTypeComposite, true}, + } + data := bytes.Repeat([]byte("server-side-part-checksum"), 1024) + + for _, test := range tests { + t.Run(test.typ.String()+"/"+test.objType, func(t *testing.T) { + object := "algorithms/" + test.typ.String() + "/" + test.objType + uploadID := newMultipartUploadHTTP(t, apiRouter, credentials, bucketName, object, + test.typ.String(), test.objType) + etag, rec := uploadPartHTTP(t, apiRouter, credentials, + bucketName, object, uploadID, 1, data, nil) + if got := rec.Header().Get(test.typ.Key()); got != "" { + t.Fatalf("%s: UploadPart returned server-computed checksum %q", instanceType, got) + } + + want := mustChecksum(t, test.typ, data) + listed := listPartsHTTP(t, apiRouter, credentials, bucketName, object, uploadID, nil) + if len(listed.Parts) != 1 || partChecksum(test.typ, listed.Parts[0]) != want { + t.Fatalf("%s: ListParts checksum mismatch: %+v, want %q", instanceType, listed.Parts, want) + } + + part := CompletePart{PartNumber: 1, ETag: etag} + if test.composite { + part = completePartWithChecksum(test.typ, 1, etag, want) + } + rec = completePartsHTTP(t, apiRouter, credentials, bucketName, object, uploadID, + []CompletePart{part}, nil) + if rec.Code != http.StatusOK { + t.Fatalf("%s: CompleteMultipartUpload failed: %d %s", instanceType, rec.Code, rec.Body.String()) + } + }) + } + + t.Run("multi-part/FULL_OBJECT", func(t *testing.T) { + typ := hash.ChecksumCRC32 + parts, full := multipartChecksumTestData() + object := "algorithms/multi-part-full-object" + uploadID := newMultipartUploadHTTP(t, apiRouter, credentials, bucketName, object, + typ.String(), xhttp.AmzChecksumTypeFullObject) + etags := make([]string, len(parts)) + for i, data := range parts { + etag, rec := uploadPartHTTP(t, apiRouter, credentials, + bucketName, object, uploadID, i+1, data, nil) + if got := rec.Header().Get(typ.Key()); got != "" { + t.Fatalf("%s: UploadPart returned server-computed checksum %q", instanceType, got) + } + etags[i] = etag + } + + listed := listPartsHTTP(t, apiRouter, credentials, bucketName, object, uploadID, nil) + if len(listed.Parts) != len(parts) { + t.Fatalf("%s: ListParts returned %d parts, want %d", instanceType, len(listed.Parts), len(parts)) + } + for i, part := range listed.Parts { + if got, want := partChecksum(typ, part), mustChecksum(t, typ, parts[i]); got != want { + t.Fatalf("%s: part %d checksum %q, want %q", instanceType, i+1, got, want) + } + } + + complete := make([]CompletePart, len(etags)) + for i, etag := range etags { + complete[i] = CompletePart{PartNumber: i + 1, ETag: etag} + } + rec := completePartsHTTP(t, apiRouter, credentials, bucketName, object, uploadID, complete, + map[string]string{ + typ.Key(): mustChecksum(t, typ, full), + xhttp.AmzChecksumType: xhttp.AmzChecksumTypeFullObject, + }) + if rec.Code != http.StatusOK { + t.Fatalf("%s: multi-part CompleteMultipartUpload failed: %d %s", instanceType, rec.Code, rec.Body.String()) + } + }) + + t.Run("zero-length-part", func(t *testing.T) { + typ := hash.ChecksumCRC32 + object := "algorithms/zero-length" + uploadID := newMultipartUploadHTTP(t, apiRouter, credentials, bucketName, object, + typ.String(), xhttp.AmzChecksumTypeFullObject) + etag, _ := uploadPartHTTP(t, apiRouter, credentials, + bucketName, object, uploadID, 1, nil, nil) + listed := listPartsHTTP(t, apiRouter, credentials, bucketName, object, uploadID, nil) + if len(listed.Parts) != 1 || partChecksum(typ, listed.Parts[0]) != mustChecksum(t, typ, nil) { + t.Fatalf("%s: zero-length ListParts checksum mismatch: %+v", instanceType, listed.Parts) + } + rec := completePartsHTTP(t, apiRouter, credentials, bucketName, object, uploadID, + []CompletePart{{PartNumber: 1, ETag: etag}}, nil) + if rec.Code != http.StatusOK { + t.Fatalf("%s: zero-length CompleteMultipartUpload failed: %d %s", instanceType, rec.Code, rec.Body.String()) + } + }) + + t.Run("overwrite-part-checksum", func(t *testing.T) { + typ := hash.ChecksumCRC32 + object := "algorithms/overwrite" + uploadID := newMultipartUploadHTTP(t, apiRouter, credentials, bucketName, object, + typ.String(), xhttp.AmzChecksumTypeFullObject) + first := []byte("first part contents") + second := []byte("replacement part contents") + uploadPartHTTP(t, apiRouter, credentials, bucketName, object, uploadID, 1, first, nil) + etag, _ := uploadPartHTTP(t, apiRouter, credentials, bucketName, object, uploadID, 1, second, nil) + listed := listPartsHTTP(t, apiRouter, credentials, bucketName, object, uploadID, nil) + if len(listed.Parts) != 1 || partChecksum(typ, listed.Parts[0]) != mustChecksum(t, typ, second) { + t.Fatalf("%s: overwritten ListParts checksum mismatch: %+v", instanceType, listed.Parts) + } + rec := completePartsHTTP(t, apiRouter, credentials, bucketName, object, uploadID, + []CompletePart{{PartNumber: 1, ETag: etag}}, nil) + if rec.Code != http.StatusOK { + t.Fatalf("%s: overwritten CompleteMultipartUpload failed: %d %s", instanceType, rec.Code, rec.Body.String()) + } + }) + + t.Run("copy/SHA256/COMPOSITE", func(t *testing.T) { + typ := hash.ChecksumSHA256 + source := "algorithms/copy-source" + if _, err := obj.PutObject(t.Context(), bucketName, source, + mustGetPutObjReader(t, bytes.NewReader(data), int64(len(data)), "", ""), ObjectOptions{}); err != nil { + t.Fatalf("%s: source PutObject failed: %v", instanceType, err) + } + object := "algorithms/copy-SHA256" + uploadID := newMultipartUploadHTTP(t, apiRouter, credentials, bucketName, object, + typ.String(), xhttp.AmzChecksumTypeComposite) + start, end := 7, len(data)-9 + response := copyPartWithoutChecksumHTTP(t, apiRouter, credentials, + bucketName, source, object, uploadID, "bytes="+strconv.Itoa(start)+"-"+strconv.Itoa(end-1), nil) + want := mustChecksum(t, typ, data[start:end]) + if got := copyPartChecksum(typ, response); got != want { + t.Fatalf("%s: UploadPartCopy checksum %q, want %q", instanceType, got, want) + } + + part := completePartWithChecksum(typ, 1, canonicalizeETag(response.ETag), want) + rec := completePartsHTTP(t, apiRouter, credentials, bucketName, object, uploadID, + []CompletePart{part}, nil) + if rec.Code != http.StatusOK { + t.Fatalf("%s: copied CompleteMultipartUpload failed: %d %s", instanceType, rec.Code, rec.Body.String()) + } + }) +} + +func TestAPIUploadPartServerSideChecksumDoesNotMaskClientErrors(t *testing.T) { + defer DetectTestLeak(t)() + ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{ + t: t, + objAPITest: testAPIUploadPartServerSideChecksumDoesNotMaskClientErrors, + endpoints: []string{"PutObjectPart", "NewMultipart", "ListObjectParts"}, + }) +} + +func testAPIUploadPartServerSideChecksumDoesNotMaskClientErrors(_ ObjectLayer, instanceType, bucketName string, + apiRouter http.Handler, credentials auth.Credentials, t *testing.T, +) { + data := []byte("client checksum must remain authoritative") + + t.Run("correct-value", func(t *testing.T) { + typ := hash.ChecksumCRC32 + object := "errors/correct-value" + uploadID := newMultipartUploadHTTP(t, apiRouter, credentials, bucketName, object, + typ.String(), xhttp.AmzChecksumTypeFullObject) + want := mustChecksum(t, typ, data) + _, rec := uploadPartHTTP(t, apiRouter, credentials, + bucketName, object, uploadID, 1, data, map[string]string{typ.Key(): want}) + if got := rec.Header().Get(typ.Key()); got != want { + t.Fatalf("%s: client checksum response %q, want %q", instanceType, got, want) + } + listed := listPartsHTTP(t, apiRouter, credentials, bucketName, object, uploadID, nil) + if len(listed.Parts) != 1 || partChecksum(typ, listed.Parts[0]) != want { + t.Fatalf("%s: client checksum ListParts mismatch: %+v", instanceType, listed.Parts) + } + }) + + t.Run("wrong-algorithm", func(t *testing.T) { + object := "errors/wrong-algorithm" + uploadID := newMultipartUploadHTTP(t, apiRouter, credentials, bucketName, object, + hash.ChecksumCRC32.String(), xhttp.AmzChecksumTypeFullObject) + req, err := newTestSignedRequestV4(http.MethodPut, + getPutObjectPartURL("", bucketName, object, uploadID, "1"), + int64(len(data)), bytes.NewReader(data), credentials.AccessKey, credentials.SecretKey, + map[string]string{hash.ChecksumSHA256.Key(): mustChecksum(t, hash.ChecksumSHA256, data)}) + if err != nil { + t.Fatalf("failed to build UploadPart request: %v", err) + } + rec := httptest.NewRecorder() + apiRouter.ServeHTTP(rec, req) + if rec.Code != http.StatusBadRequest || apiErrorCode(t, rec) != "InvalidArgument" { + t.Fatalf("%s: wrong algorithm returned %d %s", instanceType, rec.Code, rec.Body.String()) + } + }) + + t.Run("wrong-value", func(t *testing.T) { + object := "errors/wrong-value" + uploadID := newMultipartUploadHTTP(t, apiRouter, credentials, bucketName, object, + hash.ChecksumCRC32.String(), xhttp.AmzChecksumTypeFullObject) + req, err := newTestSignedRequestV4(http.MethodPut, + getPutObjectPartURL("", bucketName, object, uploadID, "1"), + int64(len(data)), bytes.NewReader(data), credentials.AccessKey, credentials.SecretKey, + map[string]string{hash.ChecksumCRC32.Key(): mustChecksum(t, hash.ChecksumCRC32, []byte("wrong"))}) + if err != nil { + t.Fatalf("failed to build UploadPart request: %v", err) + } + rec := httptest.NewRecorder() + apiRouter.ServeHTTP(rec, req) + if rec.Code != http.StatusBadRequest || apiErrorCode(t, rec) != "XAmzContentChecksumMismatch" { + t.Fatalf("%s: wrong value returned %d %s", instanceType, rec.Code, rec.Body.String()) + } + }) +} + +func TestAPIUploadPartServerSideChecksumSSEC(t *testing.T) { + defer DetectTestLeak(t)() + ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{ + t: t, + objAPITest: testAPIUploadPartServerSideChecksumSSEC, + endpoints: []string{"PutObjectPart", "NewMultipart", "CompleteMultipart"}, + }) +} + +func testAPIUploadPartServerSideChecksumSSEC(_ ObjectLayer, instanceType, bucketName string, apiRouter http.Handler, + credentials auth.Credentials, t *testing.T, +) { + globalIsTLS = true + defer func() { globalIsTLS = false }() + + key := bytes.Repeat([]byte{0x2a}, 32) + keyMD5 := md5.Sum(key) + ssecHeaders := map[string]string{ + xhttp.AmzServerSideEncryptionCustomerAlgorithm: xhttp.AmzEncryptionAES, + xhttp.AmzServerSideEncryptionCustomerKey: base64.StdEncoding.EncodeToString(key), + xhttp.AmzServerSideEncryptionCustomerKeyMD5: base64.StdEncoding.EncodeToString(keyMD5[:]), + } + initHeaders := map[string]string{ + xhttp.AmzChecksumAlgo: hash.ChecksumCRC32.String(), + xhttp.AmzChecksumType: xhttp.AmzChecksumTypeFullObject, + xhttp.AmzServerSideEncryptionCustomerAlgorithm: ssecHeaders[xhttp.AmzServerSideEncryptionCustomerAlgorithm], + xhttp.AmzServerSideEncryptionCustomerKey: ssecHeaders[xhttp.AmzServerSideEncryptionCustomerKey], + xhttp.AmzServerSideEncryptionCustomerKeyMD5: ssecHeaders[xhttp.AmzServerSideEncryptionCustomerKeyMD5], + } + object := "checksums/ssec" + req, err := newTestSignedRequestV4(http.MethodPost, getNewMultipartURL("", bucketName, object), + 0, nil, credentials.AccessKey, credentials.SecretKey, initHeaders) + if err != nil { + t.Fatalf("failed to build NewMultipartUpload request: %v", err) + } + rec := httptest.NewRecorder() + apiRouter.ServeHTTP(rec, req) + if rec.Code != http.StatusOK { + t.Fatalf("%s: NewMultipartUpload failed: %d %s", instanceType, rec.Code, rec.Body.String()) + } + var initiated InitiateMultipartUploadResponse + if err := xml.Unmarshal(rec.Body.Bytes(), &initiated); err != nil { + t.Fatalf("failed to decode NewMultipartUpload response: %v", err) + } + + data := bytes.Repeat([]byte("ssec-checksum-plaintext"), 4096) + etag, uploadRec := uploadPartHTTP(t, apiRouter, credentials, + bucketName, object, initiated.UploadID, 1, data, ssecHeaders) + if got := uploadRec.Header().Get(hash.ChecksumCRC32.Key()); got != "" { + t.Fatalf("%s: UploadPart returned server-computed checksum %q", instanceType, got) + } + + completeHeaders := map[string]string{ + xhttp.AmzChecksumCRC32: mustChecksum(t, hash.ChecksumCRC32, data), + xhttp.AmzChecksumType: xhttp.AmzChecksumTypeFullObject, + xhttp.AmzServerSideEncryptionCustomerAlgorithm: ssecHeaders[xhttp.AmzServerSideEncryptionCustomerAlgorithm], + xhttp.AmzServerSideEncryptionCustomerKey: ssecHeaders[xhttp.AmzServerSideEncryptionCustomerKey], + xhttp.AmzServerSideEncryptionCustomerKeyMD5: ssecHeaders[xhttp.AmzServerSideEncryptionCustomerKeyMD5], + } + rec = completePartsHTTP(t, apiRouter, credentials, bucketName, object, initiated.UploadID, + []CompletePart{{PartNumber: 1, ETag: etag}}, completeHeaders) + if rec.Code != http.StatusOK { + t.Fatalf("%s: CompleteMultipartUpload failed: %d %s", instanceType, rec.Code, rec.Body.String()) + } +} + +func TestAPIUploadPartServerSideChecksumSSES3(t *testing.T) { + defer DetectTestLeak(t)() + ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{ + t: t, + objAPITest: testAPIUploadPartServerSideChecksumSSES3, + endpoints: []string{"PutObjectPart", "NewMultipart", "CompleteMultipart"}, + }) +} + +func testAPIUploadPartServerSideChecksumSSES3(_ ObjectLayer, instanceType, bucketName string, apiRouter http.Handler, + credentials auth.Credentials, t *testing.T, +) { + KMS, err := kms.ParseSecretKey("my-minio-key:5lF+0pJM0OWwlQrvK2S/I7W9mO4a6rJJI7wzj7v09cw=") + if err != nil { + t.Fatal(err) + } + GlobalKMS = KMS + defer func() { GlobalKMS = nil }() + + object := "checksums/sse-s3" + initHeaders := map[string]string{ + xhttp.AmzChecksumAlgo: hash.ChecksumCRC32.String(), + xhttp.AmzChecksumType: xhttp.AmzChecksumTypeFullObject, + xhttp.AmzServerSideEncryption: xhttp.AmzEncryptionAES, + } + req, err := newTestSignedRequestV4(http.MethodPost, getNewMultipartURL("", bucketName, object), + 0, nil, credentials.AccessKey, credentials.SecretKey, initHeaders) + if err != nil { + t.Fatalf("failed to build NewMultipartUpload request: %v", err) + } + rec := httptest.NewRecorder() + apiRouter.ServeHTTP(rec, req) + if rec.Code != http.StatusOK { + t.Fatalf("%s: NewMultipartUpload failed: %d %s", instanceType, rec.Code, rec.Body.String()) + } + var initiated InitiateMultipartUploadResponse + if err := xml.Unmarshal(rec.Body.Bytes(), &initiated); err != nil { + t.Fatalf("failed to decode NewMultipartUpload response: %v", err) + } + + data := bytes.Repeat([]byte("sse-s3-checksum-plaintext"), 4096) + etag, uploadRec := uploadPartHTTP(t, apiRouter, credentials, + bucketName, object, initiated.UploadID, 1, data, nil) + if got := uploadRec.Header().Get(hash.ChecksumCRC32.Key()); got != "" { + t.Fatalf("%s: UploadPart returned server-computed checksum %q", instanceType, got) + } + + rec = completePartsHTTP(t, apiRouter, credentials, bucketName, object, initiated.UploadID, + []CompletePart{{PartNumber: 1, ETag: etag}}, map[string]string{ + xhttp.AmzChecksumCRC32: mustChecksum(t, hash.ChecksumCRC32, data), + xhttp.AmzChecksumType: xhttp.AmzChecksumTypeFullObject, + }) + if rec.Code != http.StatusOK { + t.Fatalf("%s: CompleteMultipartUpload failed: %d %s", instanceType, rec.Code, rec.Body.String()) + } +} diff --git a/cmd/erasure-multipart.go b/cmd/erasure-multipart.go index f73b7da4f..9de330e28 100644 --- a/cmd/erasure-multipart.go +++ b/cmd/erasure-multipart.go @@ -39,9 +39,9 @@ import ( xhttp "github.com/minio/minio/internal/http" xioutil "github.com/minio/minio/internal/ioutil" "github.com/minio/minio/internal/logger" - "github.com/minio/pkg/v3/mimedb" - "github.com/minio/pkg/v3/sync/errgroup" "github.com/minio/sio" + "github.com/pgsty/silo-pkg/v3/mimedb" + "github.com/pgsty/silo-pkg/v3/sync/errgroup" ) func (er erasureObjects) getUploadIDDir(bucket, object, uploadID string) string { @@ -597,12 +597,15 @@ func (er erasureObjects) PutObjectPart(ctx context.Context, bucket, object, uplo onlineDisks := er.getDisks() writeQuorum := fi.WriteQuorum(er.defaultWQuorum()) - if cs := fi.Metadata[hash.MinIOMultipartChecksum]; cs != "" { - if r.ContentCRCType().String() != cs { + expectedChecksumType, checksumEnabled := multipartChecksumType(fi.Metadata) + if checksumEnabled { + got := r.contentChecksumType() + if !expectedChecksumType.IsSet() || !got.IsSet() || got.Base() != expectedChecksumType { return pi, InvalidArgument{ Bucket: bucket, Object: fi.Name, - Err: fmt.Errorf("checksum missing, want %q, got %q", cs, r.ContentCRCType().String()), + Err: fmt.Errorf("checksum missing, want %q, got %q", + fi.Metadata[hash.MinIOMultipartChecksum], got.String()), } } } @@ -707,24 +710,37 @@ func (er erasureObjects) PutObjectPart(ctx context.Context, bucket, object, uplo } actualSize := data.ActualSize() - if actualSize < 0 { - _, encrypted := crypto.IsEncrypted(fi.Metadata) - compressed := fi.IsCompressed() - switch { - case compressed: - // ... nothing changes for compressed stream. - // if actualSize is -1 we have no known way to - // determine what is the actualSize. - case encrypted: - decSize, err := sio.DecryptedSize(uint64(n)) - if err == nil { - actualSize = int64(decSize) - } - default: + _, encrypted := crypto.IsEncrypted(fi.Metadata) + compressed := fi.IsCompressed() + switch { + case compressed: + // ... nothing changes for compressed stream. + // if actualSize is -1 we have no known way to + // determine what is the actualSize. + case encrypted: + // The uploaded length of an encrypted part is always derivable from the + // bytes just written, and the caller's value cannot be trusted: trusted + // SSE-C replication and the data movement paths hand over the ciphertext + // length. Derive it with the arithmetic the read path applies to + // part.Size, so the stored value matches how the part is read back. + decSize, err := sio.DecryptedSize(uint64(n)) + if err != nil { + return pi, toObjectErr(errObjectTampered, bucket, object, uploadID) + } + actualSize = int64(decSize) + default: + if actualSize < 0 { actualSize = n } } + partChecksums := r.contentChecksum() + if checksumEnabled && partChecksums[expectedChecksumType.String()] == "" { + err := fmt.Errorf("internal error: checksum missing after reading part, want %q", expectedChecksumType.String()) + bugLogIf(ctx, err) + return pi, toObjectErr(err, bucket, object, uploadID) + } + partInfo := ObjectPartInfo{ Number: partID, ETag: md5hex, @@ -732,7 +748,7 @@ func (er erasureObjects) PutObjectPart(ctx context.Context, bucket, object, uplo ActualSize: actualSize, ModTime: UTCNow(), Index: index, - Checksums: r.ContentCRC(), + Checksums: partChecksums, } partFI, err := partInfo.MarshalMsg(nil) @@ -1098,7 +1114,7 @@ func (er erasureObjects) CompleteMultipartUpload(ctx context.Context, bucket str auditObjectErasureSet(ctx, "CompleteMultipartUpload", object, &er) } - if opts.CheckPrecondFn != nil { + if opts.CheckPrecondFn != nil || opts.ReplicaLockReconcile { if !opts.NoLock { ns := er.NewNSLock(bucket, object) lkctx, err := ns.GetLock(ctx, globalOperationTimeout) @@ -1110,18 +1126,24 @@ func (er erasureObjects) CompleteMultipartUpload(ctx context.Context, bucket str opts.NoLock = true } - obj, err := er.getObjectInfo(ctx, bucket, object, opts) - if err == nil && opts.CheckPrecondFn(obj) { - return ObjectInfo{}, PreConditionFailed{} - } - if err != nil && !isErrVersionNotFound(err) && !isErrObjectNotFound(err) { - return ObjectInfo{}, err - } + // The Object Lock reconcile below needs the version being committed, read + // after checkUploadIDExists, so only the precondition read happens here; + // both run under this same write lock, held until the version is renamed + // into place. + if opts.CheckPrecondFn != nil { + obj, err := er.getObjectInfo(ctx, bucket, object, opts) + if err == nil && opts.CheckPrecondFn(obj) { + return ObjectInfo{}, PreConditionFailed{} + } + if err != nil && !isErrVersionNotFound(err) && !isErrObjectNotFound(err) { + return ObjectInfo{}, err + } - // if object doesn't exist return error for If-Match conditional requests - // If-None-Match should be allowed to proceed for non-existent objects - if err != nil && opts.HasIfMatch && (isErrObjectNotFound(err) || isErrVersionNotFound(err)) { - return ObjectInfo{}, err + // if object doesn't exist return error for If-Match conditional requests + // If-None-Match should be allowed to proceed for non-existent objects + if err != nil && opts.HasIfMatch && (isErrObjectNotFound(err) || isErrVersionNotFound(err)) { + return ObjectInfo{}, err + } } } @@ -1133,6 +1155,42 @@ func (er erasureObjects) CompleteMultipartUpload(ctx context.Context, bucket str return oi, toObjectErr(err, bucket, object, uploadID) } + // A trusted SSE-C replica completion re-orders the Object Lock carried in the + // upload metadata against the version it is about to replace, read on this + // erasure set under the write lock held above, so a hold or retention that + // reached the version after this upload was initiated is not rolled back at + // completion (issue #120). Scoped to SSE-C uploads, the only ones this issue + // routes through completion. + // + // Scope: correct for a single erasure set. A multi-pool deployment (duplicate + // versions across pools, ModTime ties, cross-pool lock authority) is out of + // scope and tracked in pgsty/silo#133. + if opts.ReplicaLockReconcile && crypto.SSEC.IsEncrypted(fi.Metadata) { + // A persisted upload records the null version as an empty VersionID; look + // it up as the null version so the reconcile reads the addressed version's + // stored lock, not the latest version's. + lookupVersionID := fi.VersionID + if lookupVersionID == "" { + lookupVersionID = nullVersionID + } + curr, gerr := er.getObjectInfo(ctx, bucket, object, ObjectOptions{ + VersionID: lookupVersionID, + Versioned: opts.Versioned, + VersionSuspended: opts.VersionSuspended, + NoLock: true, + }) + switch { + case gerr == nil: + reconcileStoredObjectLock(fi.Metadata, storedObjectLockState(curr.UserDefined)) + case isErrVersionNotFound(gerr) || isErrObjectNotFound(gerr): + // No existing version to order against: keep the upload's own accepted + // lock, including a pre-upgrade upload that persisted values without + // their ordering timestamps. + default: + return oi, toObjectErr(gerr, bucket, object) + } + } + uploadIDPath := er.getUploadIDDir(bucket, object, uploadID) onlineDisks := er.getDisks() writeQuorum := fi.WriteQuorum(er.defaultWQuorum()) @@ -1163,11 +1221,20 @@ func (er erasureObjects) CompleteMultipartUpload(ctx context.Context, bucket str var checksumType hash.ChecksumType if cs := fi.Metadata[hash.MinIOMultipartChecksum]; cs != "" { checksumType = hash.NewChecksumType(cs, fi.Metadata[hash.MinIOMultipartChecksumType]) - if opts.WantChecksum != nil && !opts.WantChecksum.Type.Is(checksumType) { - return oi, InvalidArgument{ - Bucket: bucket, - Object: fi.Name, - Err: fmt.Errorf("checksum type mismatch. got %q (%s) expected %q (%s)", checksumType.String(), checksumType.ObjType(), opts.WantChecksum.Type.String(), opts.WantChecksum.Type.ObjType()), + expectedType := checksumType | hash.ChecksumMultipart | hash.ChecksumIncludesMultipart + if opts.WantChecksum != nil { + providedType := opts.WantChecksum.Type | hash.ChecksumMultipart | hash.ChecksumIncludesMultipart + if providedType.Base() != expectedType.Base() { + return oi, InvalidArgument{ + Bucket: bucket, + Object: fi.Name, + Err: fmt.Errorf("checksum algorithm mismatch. got %q expected %q", providedType.String(), expectedType.String()), + } + } + } + if opts.wantChecksumType != "" { + if opts.wantChecksumType != expectedType.ObjType() { + return oi, completeMultipartChecksumTypeMismatch(opts.wantChecksumType, expectedType.ObjType()) } } checksumType |= hash.ChecksumMultipart | hash.ChecksumIncludesMultipart @@ -1298,19 +1365,18 @@ func (er erasureObjects) CompleteMultipartUpload(ctx context.Context, bucket str break } } - // Part checksums are optional in the CompleteMultipartUpload body when - // the upload was created with a full object checksum type: clients send - // the object level checksum instead and do not retain part checksums. - // A part that carries any checksum at all is still validated against - // what we stored - including one sent under the wrong algorithm, which - // cannot match and is rejected. The object level checksum, if supplied, - // is verified against the merged part checksums below. - allowMissingPartCS := checksumType.FullObjectRequested() && !suppliedAnyCS - if !allowMissingPartCS && gotCS != crc { + // Full object completions may omit part checksums. Composite + // completions may not. Any checksum that is supplied is still + // validated, including one sent under the wrong algorithm. + if !suppliedAnyCS { + if !checksumType.FullObjectRequested() { + return oi, missingPartChecksum(checksumType.String(), part.PartNumber) + } + } else if gotCS != crc { return oi, InvalidPart{ PartNumber: part.PartNumber, - ExpETag: gotCS, - GotETag: crc, + ExpETag: crc, + GotETag: gotCS, } } cs := hash.NewChecksumString(checksumType.String(), crc) @@ -1360,15 +1426,14 @@ func (er erasureObjects) CompleteMultipartUpload(ctx context.Context, bucket str if opts.WantChecksum != nil { if checksumType.FullObjectRequested() { if opts.WantChecksum.Encoded != checksum.Encoded { - err := hash.ChecksumMismatch{ - Want: opts.WantChecksum.Encoded, - Got: checksum.Encoded, - } - return oi, err + return oi, completeMultipartChecksumMismatch(checksumType.String()) } } else { err := opts.WantChecksum.Matches(checksumCombined, len(parts)) if err != nil { + if hash.IsChecksumMismatch(err) { + return oi, completeMultipartChecksumMismatch(checksumType.String()) + } return oi, err } } diff --git a/cmd/erasure-object-conditional-delete_test.go b/cmd/erasure-object-conditional-delete_test.go new file mode 100644 index 000000000..e0e1857ef --- /dev/null +++ b/cmd/erasure-object-conditional-delete_test.go @@ -0,0 +1,295 @@ +// Copyright (c) 2015-2025 MinIO, Inc. +// +// This file is part of MinIO Object Storage stack +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package cmd + +import ( + "bytes" + "context" + "net/http" + "testing" +) + +// TestDeleteObjectConditional verifies that a conditional DeleteObject +// (If-Match, wired through opts.CheckPrecondFn) is evaluated atomically at the +// object layer: a non-matching ETag must fail with PreConditionFailed and leave +// the object intact, a matching ETag must delete it, and an If-Match against a +// missing object must return a not-found error rather than silently succeeding. +func TestDeleteObjectConditional(t *testing.T) { + ctx := context.Background() + + obj, fsDirs, err := prepareErasure16(ctx) + if err != nil { + t.Fatal(err) + } + defer obj.Shutdown(context.Background()) + defer removeRoots(fsDirs) + + bucket := "test-bucket" + object := "test-object" + + if err = obj.MakeBucket(ctx, bucket, MakeBucketOptions{}); err != nil { + t.Fatal(err) + } + + if _, err = obj.PutObject(ctx, bucket, object, + mustGetPutObjReader(t, bytes.NewReader([]byte("test-value")), + int64(len("test-value")), "", ""), ObjectOptions{}); err != nil { + t.Fatal(err) + } + + objInfo, err := obj.GetObjectInfo(ctx, bucket, object, ObjectOptions{}) + if err != nil { + t.Fatal(err) + } + existingETag := objInfo.ETag + + // If-Match with a wrong ETag must fail and preserve the object. + t.Run("wrong-etag-precondition-failed", func(t *testing.T) { + opts := ObjectOptions{ + HasIfMatch: true, + CheckPrecondFn: func(oi ObjectInfo) bool { + return !isETagEqual(oi.ETag, "wrong-etag") + }, + } + if _, err := obj.DeleteObject(ctx, bucket, object, opts); !isErrPreconditionFailed(err) { + t.Errorf("expected PreConditionFailed, got: %v", err) + } + if _, err := obj.GetObjectInfo(ctx, bucket, object, ObjectOptions{}); err != nil { + t.Errorf("object must still exist after a failed conditional delete, got: %v", err) + } + }) + + // If-Match against a missing object must return a not-found error. + t.Run("missing-object-not-found", func(t *testing.T) { + opts := ObjectOptions{ + HasIfMatch: true, + CheckPrecondFn: func(oi ObjectInfo) bool { + return !isETagEqual(oi.ETag, existingETag) + }, + } + _, err := obj.DeleteObject(ctx, bucket, "does-not-exist", opts) + if !isErrObjectNotFound(err) && !isErrVersionNotFound(err) { + t.Errorf("expected ObjectNotFound/VersionNotFound, got: %v", err) + } + }) + + // If-Match with the correct ETag must delete the object (run last). + t.Run("correct-etag-succeeds", func(t *testing.T) { + opts := ObjectOptions{ + HasIfMatch: true, + CheckPrecondFn: func(oi ObjectInfo) bool { + return !isETagEqual(oi.ETag, existingETag) + }, + } + if _, err := obj.DeleteObject(ctx, bucket, object, opts); err != nil { + t.Errorf("expected a successful delete with matching ETag, got: %v", err) + } + if _, err := obj.GetObjectInfo(ctx, bucket, object, ObjectOptions{}); !isErrObjectNotFound(err) { + t.Errorf("object must be removed after a matching conditional delete, got: %v", err) + } + }) +} + +// TestDeleteObjectConditionalWithReadQuorumFailure verifies that a conditional +// (If-Match) DeleteObject does NOT proceed when the object's current state +// cannot be read due to read-quorum loss: without a verified ETag the delete +// must fail rather than remove the object blindly. +func TestDeleteObjectConditionalWithReadQuorumFailure(t *testing.T) { + ctx := context.Background() + + obj, fsDirs, err := prepareErasure16(ctx) + if err != nil { + t.Fatal(err) + } + defer obj.Shutdown(context.Background()) + defer removeRoots(fsDirs) + + z := obj.(*erasureServerPools) + xl := z.serverPools[0].sets[0] + + bucket := "test-bucket" + object := "test-object" + + if err = obj.MakeBucket(ctx, bucket, MakeBucketOptions{}); err != nil { + t.Fatal(err) + } + + if _, err = obj.PutObject(ctx, bucket, object, + mustGetPutObjReader(t, bytes.NewReader([]byte("test-value")), + int64(len("test-value")), "", ""), ObjectOptions{}); err != nil { + t.Fatal(err) + } + + objInfo, err := obj.GetObjectInfo(ctx, bucket, object, ObjectOptions{}) + if err != nil { + t.Fatal(err) + } + existingETag := objInfo.ETag + + // Simulate read-quorum loss by taking 8 of 16 disks offline (EC 8+8). + erasureDisks := xl.getDisks() + z.serverPools[0].erasureDisksMu.Lock() + xl.getDisks = func() []StorageAPI { + for i := range erasureDisks[:8] { + erasureDisks[i] = nil + } + return erasureDisks + } + z.serverPools[0].erasureDisksMu.Unlock() + + // Even with the correct ETag we must not delete: the current state (hence the + // ETag) cannot be verified under read-quorum loss. + opts := ObjectOptions{ + HasIfMatch: true, + CheckPrecondFn: func(oi ObjectInfo) bool { + return !isETagEqual(oi.ETag, existingETag) + }, + } + if _, err := obj.DeleteObject(ctx, bucket, object, opts); err == nil { + t.Error("expected an error for a conditional delete under read-quorum loss, got nil (object may have been deleted without ETag verification)") + } +} + +// TestDeleteObjectConditionalVersioned verifies conditional DeleteObject on a +// versioned bucket, where the precondition is evaluated at the server-pool layer +// against the version that will actually be removed: +// - If-Match "*" when the latest version is a delete marker must fail (412), +// because there is no live object to match. +// - An explicit versionId If-Match is evaluated against the addressed version, +// not the latest one (match deletes it, mismatch is refused). +// - An If-Match against a missing version returns VersionNotFound, which the +// handler maps to NoSuchVersion. +func TestDeleteObjectConditionalVersioned(t *testing.T) { + ctx := context.Background() + + obj, fsDirs, err := prepareErasure16(ctx) + if err != nil { + t.Fatal(err) + } + defer obj.Shutdown(context.Background()) + defer removeRoots(fsDirs) + + bucket := "test-bucket" + + if err = obj.MakeBucket(ctx, bucket, MakeBucketOptions{VersioningEnabled: true}); err != nil { + t.Fatal(err) + } + versioned := globalBucketVersioningSys.PrefixEnabled(bucket, "any") + if !versioned { + t.Fatalf("expected versioning to be enabled on %q", bucket) + } + + put := func(object, content string) ObjectInfo { + oi, perr := obj.PutObject(ctx, bucket, object, + mustGetPutObjReader(t, bytes.NewReader([]byte(content)), int64(len(content)), "", ""), + ObjectOptions{Versioned: versioned}) + if perr != nil { + t.Fatalf("put %q: %v", object, perr) + } + return oi + } + ifMatch := func(value string) CheckPreconditionFn { + return func(oi ObjectInfo) bool { + return deleteIfMatchPreconditionFailed(http.Header{}, value, oi) + } + } + + // If-Match "*" against a delete-marker-latest must fail with 412. + t.Run("wildcard-on-delete-marker-latest", func(t *testing.T) { + object := "dm-object" + put(object, "v1") + // Create a delete marker (unconditional), making the latest a delete marker. + if _, derr := obj.DeleteObject(ctx, bucket, object, ObjectOptions{Versioned: versioned}); derr != nil { + t.Fatalf("create delete marker: %v", derr) + } + opts := ObjectOptions{Versioned: versioned, HasIfMatch: true, CheckPrecondFn: ifMatch("*")} + if _, derr := obj.DeleteObject(ctx, bucket, object, opts); !isErrPreconditionFailed(derr) { + t.Errorf("expected PreConditionFailed for If-Match:* on a delete-marker-latest, got: %v", derr) + } + }) + + // Explicit versionId is evaluated against the addressed (older) version. + t.Run("explicit-version-selection", func(t *testing.T) { + object := "ver-object" + v1 := put(object, "first") + v2 := put(object, "second-longer") // v2 is now the latest with a different ETag + if v1.ETag == v2.ETag { + t.Fatalf("test setup: versions must have distinct ETags") + } + + // Mismatch: delete v2 with v1's ETag must be refused, v2 preserved. + mismatch := ObjectOptions{Versioned: versioned, VersionID: v2.VersionID, HasIfMatch: true, CheckPrecondFn: ifMatch(v1.ETag)} + if _, derr := obj.DeleteObject(ctx, bucket, object, mismatch); !isErrPreconditionFailed(derr) { + t.Errorf("expected PreConditionFailed deleting v2 with v1 ETag, got: %v", derr) + } + if _, gerr := obj.GetObjectInfo(ctx, bucket, object, ObjectOptions{VersionID: v2.VersionID}); gerr != nil { + t.Errorf("v2 must still exist after a refused conditional delete, got: %v", gerr) + } + + // Match: delete v1 with v1's ETag must succeed even though v1 is not latest. + match := ObjectOptions{Versioned: versioned, VersionID: v1.VersionID, HasIfMatch: true, CheckPrecondFn: ifMatch(v1.ETag)} + if _, derr := obj.DeleteObject(ctx, bucket, object, match); derr != nil { + t.Errorf("expected the addressed version to be deleted, got: %v", derr) + } + if _, gerr := obj.GetObjectInfo(ctx, bucket, object, ObjectOptions{VersionID: v1.VersionID}); !isErrVersionNotFound(gerr) { + t.Errorf("v1 must be gone after a matching conditional delete, got: %v", gerr) + } + if _, gerr := obj.GetObjectInfo(ctx, bucket, object, ObjectOptions{VersionID: v2.VersionID}); gerr != nil { + t.Errorf("v2 must remain after deleting v1, got: %v", gerr) + } + }) + + // If-Match against a missing version on an EXISTING key returns VersionNotFound. + t.Run("missing-version", func(t *testing.T) { + object := "missing-version-object" + put(object, "only") + opts := ObjectOptions{Versioned: versioned, VersionID: mustGetUUID(), HasIfMatch: true, CheckPrecondFn: ifMatch("anything")} + if _, derr := obj.DeleteObject(ctx, bucket, object, opts); !isErrVersionNotFound(derr) { + t.Errorf("expected VersionNotFound for If-Match on a missing version, got: %v", derr) + } + }) + + // If-Match against a missing version on an ABSENT key must also return + // VersionNotFound (NoSuchVersion), not NoSuchKey: the request addresses a + // specific version, which does not exist regardless of the key. + t.Run("missing-version-absent-key", func(t *testing.T) { + opts := ObjectOptions{Versioned: versioned, VersionID: mustGetUUID(), HasIfMatch: true, CheckPrecondFn: ifMatch("anything")} + if _, derr := obj.DeleteObject(ctx, bucket, "never-existed", opts); !isErrVersionNotFound(derr) { + t.Errorf("expected VersionNotFound for If-Match on a version of an absent key, got: %v", derr) + } + }) + + // If-Match "*" addressing a delete-marker VERSION by id must fail with 412, + // not 405: a delete marker has no entity-tag to match. getObjectInfo returns + // the marker alongside MethodNotAllowed; the precondition runs on the marker. + t.Run("wildcard-on-explicit-delete-marker-version", func(t *testing.T) { + object := "explicit-dm-object" + put(object, "live") + dm, derr := obj.DeleteObject(ctx, bucket, object, ObjectOptions{Versioned: versioned}) + if derr != nil { + t.Fatalf("create delete marker: %v", derr) + } + if !dm.DeleteMarker || dm.VersionID == "" { + t.Fatalf("expected a delete-marker version, got DeleteMarker=%v VersionID=%q", dm.DeleteMarker, dm.VersionID) + } + opts := ObjectOptions{Versioned: versioned, VersionID: dm.VersionID, HasIfMatch: true, CheckPrecondFn: ifMatch("*")} + if _, derr := obj.DeleteObject(ctx, bucket, object, opts); !isErrPreconditionFailed(derr) { + t.Errorf("expected PreConditionFailed for If-Match:* on an addressed delete-marker version, got: %v", derr) + } + }) +} diff --git a/cmd/erasure-object.go b/cmd/erasure-object.go index 0247822e7..bade406e6 100644 --- a/cmd/erasure-object.go +++ b/cmd/erasure-object.go @@ -49,9 +49,9 @@ import ( xhttp "github.com/minio/minio/internal/http" xioutil "github.com/minio/minio/internal/ioutil" "github.com/minio/minio/internal/logger" - "github.com/minio/pkg/v3/mimedb" - "github.com/minio/pkg/v3/sync/errgroup" "github.com/minio/sio" + "github.com/pgsty/silo-pkg/v3/mimedb" + "github.com/pgsty/silo-pkg/v3/sync/errgroup" ) // list all errors which can be ignored in object operations. @@ -266,9 +266,19 @@ func (er erasureObjects) GetObjectNInfo(ctx context.Context, bucket, object stri ObjInfo: objInfo, }, err } - // Zero byte objects don't even need to further initialize pipes etc. - return NewGetObjectReaderFromReader(bytes.NewReader(nil), objInfo, opts) + gr, err = NewGetObjectReaderFromReader(bytes.NewReader(nil), objInfo, opts) + if err != nil { + return gr, err + } + // With no data, the reader above cannot authenticate an SSE-C key the + // way NewGetObjectReader does. Check it after the preconditions so zero + // and non-zero reads preserve the same error ordering. + if err := checkSSECReadKey(h, objInfo, opts); err != nil { + gr.Close() + return nil, err + } + return gr, nil } if objInfo.IsRemote() { @@ -1258,7 +1268,7 @@ func (er erasureObjects) putObject(ctx context.Context, bucket string, object st data := r.Reader - if opts.CheckPrecondFn != nil { + if opts.CheckPrecondFn != nil || opts.ReplicaLockReconcile { if !opts.NoLock { ns := er.NewNSLock(bucket, object) lkctx, err := ns.GetLock(ctx, globalOperationTimeout) @@ -1271,17 +1281,33 @@ func (er erasureObjects) putObject(ctx context.Context, bucket string, object st } obj, err := er.getObjectInfo(ctx, bucket, object, opts) - if err == nil && opts.CheckPrecondFn(obj) { - return objInfo, PreConditionFailed{} - } + // A destination read that fails for a reason other than not-found must not + // be taken as a passed precondition or as absent lock state. if err != nil && !isErrVersionNotFound(err) && !isErrObjectNotFound(err) { return objInfo, err } + if opts.CheckPrecondFn != nil { + if err == nil && opts.CheckPrecondFn(obj) { + return objInfo, PreConditionFailed{} + } + // if object doesn't exist return error for If-Match conditional requests + // If-None-Match should be allowed to proceed for non-existent objects + if err != nil && opts.HasIfMatch && (isErrObjectNotFound(err) || isErrVersionNotFound(err)) { + return objInfo, err + } + } - // if object doesn't exist return error for If-Match conditional requests - // If-None-Match should be allowed to proceed for non-existent objects - if err != nil && opts.HasIfMatch && (isErrObjectNotFound(err) || isErrVersionNotFound(err)) { - return objInfo, err + // Order this trusted SSE-C replica's Object Lock against the addressed + // version's stored state, read on this erasure set under the write lock, + // so a value that lost the ordering cannot overwrite a newer one committed + // after the handler decided (issue #120). Only reconcile against an + // existing version; on not-found the write's own accepted lock is kept. + // + // Scope: correct for a single erasure set. A multi-pool deployment + // (duplicate versions across pools, ModTime ties, cross-pool lock + // authority) is out of scope and tracked in pgsty/silo#133. + if opts.ReplicaLockReconcile && err == nil { + reconcileStoredObjectLock(opts.UserDefined, storedObjectLockState(obj.UserDefined)) } } @@ -1485,11 +1511,15 @@ func (er erasureObjects) putObject(ctx context.Context, bucket string, object st // over opts.WantChecksum. if opts.WantServerSideChecksumType.IsSet() { serverSideChecksum := r.RawServerSideChecksumResult() - if serverSideChecksum != nil { - fi.Checksum = serverSideChecksum.AppendTo(nil, nil) - if opts.EncryptFn != nil { - fi.Checksum = opts.EncryptFn("object-checksum", fi.Checksum) - } + if serverSideChecksum == nil || !serverSideChecksum.Valid() || + serverSideChecksum.Type.Base() != opts.WantServerSideChecksumType.Base() { + err := fmt.Errorf("internal error: server-side checksum missing, invalid, or mismatched after reading object, want %q", opts.WantServerSideChecksumType.String()) + bugLogIf(ctx, err) + return ObjectInfo{}, toObjectErr(err, bucket, object) + } + fi.Checksum = serverSideChecksum.AppendTo(nil, nil) + if opts.EncryptFn != nil { + fi.Checksum = opts.EncryptFn("object-checksum", fi.Checksum) } } else if fi.Checksum == nil && opts.WantChecksum != nil { // Trailing headers checksums should now be filled. diff --git a/cmd/erasure-server-pool-decom.go b/cmd/erasure-server-pool-decom.go index 834b9fb86..9e8c5386d 100644 --- a/cmd/erasure-server-pool-decom.go +++ b/cmd/erasure-server-pool-decom.go @@ -38,9 +38,9 @@ import ( "github.com/minio/minio/internal/bucket/versioning" "github.com/minio/minio/internal/hash" "github.com/minio/minio/internal/logger" - "github.com/minio/pkg/v3/console" - "github.com/minio/pkg/v3/env" - "github.com/minio/pkg/v3/workers" + "github.com/pgsty/silo-pkg/v3/console" + "github.com/pgsty/silo-pkg/v3/env" + "github.com/pgsty/silo-pkg/v3/workers" ) // PoolDecommissionInfo currently decommissioning information diff --git a/cmd/erasure-server-pool-rebalance.go b/cmd/erasure-server-pool-rebalance.go index e3349ee1e..92c6ca92c 100644 --- a/cmd/erasure-server-pool-rebalance.go +++ b/cmd/erasure-server-pool-rebalance.go @@ -39,8 +39,8 @@ import ( "github.com/minio/minio/internal/hash" xioutil "github.com/minio/minio/internal/ioutil" "github.com/minio/minio/internal/logger" - "github.com/minio/pkg/v3/env" - "github.com/minio/pkg/v3/workers" + "github.com/pgsty/silo-pkg/v3/env" + "github.com/pgsty/silo-pkg/v3/workers" ) //go:generate msgp -file $GOFILE -unexported diff --git a/cmd/erasure-server-pool.go b/cmd/erasure-server-pool.go index 28cc5d278..ac91bd74f 100644 --- a/cmd/erasure-server-pool.go +++ b/cmd/erasure-server-pool.go @@ -43,9 +43,9 @@ import ( "github.com/minio/minio/internal/config/storageclass" xioutil "github.com/minio/minio/internal/ioutil" "github.com/minio/minio/internal/logger" - "github.com/minio/pkg/v3/sync/errgroup" - "github.com/minio/pkg/v3/wildcard" - "github.com/minio/pkg/v3/workers" + "github.com/pgsty/silo-pkg/v3/sync/errgroup" + "github.com/pgsty/silo-pkg/v3/wildcard" + "github.com/pgsty/silo-pkg/v3/workers" "github.com/puzpuzpuz/xsync/v3" ) @@ -909,23 +909,57 @@ func (z *erasureServerPools) MakeBucket(ctx context.Context, bucket string, opts return err } - // If it doesn't exist we get a new, so ignore errors - meta := newBucketMetadata(bucket) - meta.SetCreatedAt(opts.CreatedAt) - if opts.LockEnabled { - meta.VersioningConfigXML = enabledBucketVersioningConfig - meta.ObjectLockConfigXML = enabledBucketObjectLockConfig + if isMinioMetaBucketName(bucket) { + meta := newBucketMetadata(bucket) + meta.SetCreatedAt(opts.CreatedAt) + if err := meta.Save(context.Background(), z); err != nil { + return toObjectErr(err, bucket) + } + globalBucketMetadataSys.Set(bucket, meta) + return nil } - if opts.VersioningEnabled { - meta.VersioningConfigXML = enabledBucketVersioningConfig - } - - if err := meta.Save(context.Background(), z); err != nil { + ctx, unlock, err := lockBucketMetadata(ctx, z, bucket) + if err != nil { + return toObjectErr(err, bucket) + } + err = func() error { + defer unlock() + meta := newBucketMetadata(bucket) + if opts.ForceCreate { + existing, err := loadBucketMetadataParse(ctx, z, bucket, true) + if err == nil { + meta = existing + } else if !errors.Is(err, errConfigNotFound) { + return err + } + } + if meta.Created.IsZero() { + meta.SetCreatedAt(opts.CreatedAt) + } + if opts.LockEnabled { + if err := enablePeerBucketVersioning(&meta, true); err != nil { + return err + } + if len(meta.ObjectLockConfigXML) == 0 { + meta.ObjectLockConfigXML = enabledBucketObjectLockConfig + meta.ObjectLockConfigUpdatedAt = meta.Created + } + } + if opts.VersioningEnabled { + if err := enablePeerBucketVersioning(&meta, opts.LockEnabled); err != nil { + return err + } + } + if err = meta.Save(bgContext(ctx), z); err != nil { + return err + } + globalBucketMetadataSys.Set(bucket, meta) + return nil + }() + if err != nil { return toObjectErr(err, bucket) } - - globalBucketMetadataSys.Set(bucket, meta) // Success. return nil @@ -1166,6 +1200,14 @@ func (z *erasureServerPools) DeleteObject(ctx context.Context, bucket string, ob } // Acquire a write lock before deleting the object. + // + // NOTE: this lock is taken at the server-pool level. The conditional + // (If-Match) precondition below relies on this lock making the read-check- + // delete sequence atomic. That holds for a single erasure set: the write + // path (PutObject) locks at the destination set, which shares this lock's + // namespace only within one set. Multi-pool conditional-delete atomicity + // (concurrent writers across pools, cross-pool version selection) is a + // separate concern tracked as a follow-up. lk := z.NewNSLock(bucket, object) lkctx, err := lk.GetLock(ctx, globalDeleteOperationTimeout) if err != nil { @@ -1208,9 +1250,55 @@ func (z *erasureServerPools) DeleteObject(ctx context.Context, bucket string, ob if _, ok := err.(InsufficientReadQuorum); ok { return objInfo, InsufficientWriteQuorum{} } + // A conditional (If-Match) delete addressing a specific version treats an + // absent key as an absent version. getPoolInfoExistingWithOpts strips + // VersionID, so a missing key surfaces ObjectNotFound here even for a + // version-scoped delete; normalize it to VersionNotFound (NoSuchVersion), + // matching this function's tail. The unconditional path is unchanged. + if opts.CheckPrecondFn != nil && opts.VersionID != "" && isErrObjectNotFound(err) { + return objInfo, VersionNotFound{Bucket: bucket, Object: object, VersionID: opts.VersionID} + } return objInfo, err } + // Evaluate the conditional (If-Match) precondition while the write lock + // acquired above is held, before the delete-marker short-circuit and before + // any version is removed, so the object cannot change between the check and + // the delete. This is scoped to a single erasure set (see the note at the + // lock above): only there do the delete lock and the write path share the + // same lock namespace, making the check-then-delete atomic. + if opts.CheckPrecondFn != nil { + // pinfo.ObjInfo is the current latest version. getPoolInfoExistingWithOpts + // intentionally strips VersionID, so for a version-scoped delete read the + // specifically addressed version and evaluate the precondition against it. + checkInfo := pinfo.ObjInfo + if opts.VersionID != "" { + vopts := opts + vopts.NoLock = true // delete lock already held above + vopts.CheckPrecondFn = nil + vi, verr := z.serverPools[pinfo.Index].GetObjectInfo(ctx, bucket, object, vopts) + if verr != nil && (!isErrMethodNotAllowed(verr) || !vi.DeleteMarker) { + // Genuine read failure for the addressed version: a missing + // version -> VersionNotFound (NoSuchVersion), read-quorum loss, etc. + return objInfo, verr + } + // verr is nil for a live version, or MethodNotAllowed with a populated + // delete-marker ObjectInfo when the addressed version is a delete + // marker. In the latter case evaluate the precondition against the + // marker, which fails any If-Match (-> 412), rather than surfacing 405. + checkInfo = vi + } else if checkInfo.Name == "" { + // The current state could not be read (e.g. read-quorum loss); refuse + // the conditional delete rather than act on an unverified precondition. + return objInfo, InsufficientReadQuorum{} + } + if opts.CheckPrecondFn(checkInfo) { + return objInfo, PreConditionFailed{} + } + // Precondition satisfied; lower layers must not re-evaluate it. + opts.CheckPrecondFn = nil + } + // Delete marker already present we are not going to create new delete markers. if pinfo.ObjInfo.DeleteMarker && opts.VersionID == "" { pinfo.ObjInfo.Name = decodeDirObject(object) @@ -1389,6 +1477,8 @@ func (z *erasureServerPools) CopyObject(ctx context.Context, srcBucket, srcObjec } } + // CopyObjectHandler predicts the outcome of this decision in + // copyRewritesObjectData(); keep the two in sync. if cpSrcDstSame && srcInfo.metadataOnly { // Version ID is set for the destination and source == destination version ID. if dstOpts.VersionID != "" && srcOpts.VersionID == dstOpts.VersionID { diff --git a/cmd/erasure-sets.go b/cmd/erasure-sets.go index 95a7ed339..6ad9ece65 100644 --- a/cmd/erasure-sets.go +++ b/cmd/erasure-sets.go @@ -37,8 +37,8 @@ import ( "github.com/minio/minio-go/v7/pkg/tags" "github.com/minio/minio/internal/dsync" "github.com/minio/minio/internal/logger" - "github.com/minio/pkg/v3/console" - "github.com/minio/pkg/v3/sync/errgroup" + "github.com/pgsty/silo-pkg/v3/console" + "github.com/pgsty/silo-pkg/v3/sync/errgroup" "github.com/puzpuzpuz/xsync/v3" ) @@ -839,6 +839,8 @@ func (s *erasureSets) CopyObject(ctx context.Context, srcBucket, srcObject, dstB cpSrcDstSame := srcSet == dstSet // Check if this request is only metadata update. + // CopyObjectHandler predicts the outcome of this decision in + // copyRewritesObjectData(); keep the two in sync. if cpSrcDstSame && srcInfo.metadataOnly { // Version ID is set for the destination and source == destination version ID. // perform an in-place update. diff --git a/cmd/erasure.go b/cmd/erasure.go index 4e6674c35..3eb78ff6c 100644 --- a/cmd/erasure.go +++ b/cmd/erasure.go @@ -32,7 +32,7 @@ import ( "github.com/minio/madmin-go/v3" "github.com/minio/minio/internal/dsync" xioutil "github.com/minio/minio/internal/ioutil" - "github.com/minio/pkg/v3/sync/errgroup" + "github.com/pgsty/silo-pkg/v3/sync/errgroup" ) // list all errors that can be ignore in a bucket operation. diff --git a/cmd/event-notification.go b/cmd/event-notification.go index ceda47ef9..4cec5be6e 100644 --- a/cmd/event-notification.go +++ b/cmd/event-notification.go @@ -29,7 +29,7 @@ import ( "github.com/minio/minio/internal/event" xhttp "github.com/minio/minio/internal/http" "github.com/minio/minio/internal/pubsub" - "github.com/minio/pkg/v3/policy" + "github.com/pgsty/silo-pkg/v3/policy" ) // EventNotifier - notifies external systems about events in MinIO. diff --git a/cmd/format-erasure.go b/cmd/format-erasure.go index 09031d4ed..ef730d739 100644 --- a/cmd/format-erasure.go +++ b/cmd/format-erasure.go @@ -32,7 +32,7 @@ import ( "github.com/minio/minio/internal/config/storageclass" xioutil "github.com/minio/minio/internal/ioutil" "github.com/minio/minio/internal/logger" - "github.com/minio/pkg/v3/sync/errgroup" + "github.com/pgsty/silo-pkg/v3/sync/errgroup" ) const ( diff --git a/cmd/ftp-server-driver.go b/cmd/ftp-server-driver.go index 7f21eeb8d..f04dff921 100644 --- a/cmd/ftp-server-driver.go +++ b/cmd/ftp-server-driver.go @@ -36,7 +36,7 @@ import ( "github.com/minio/minio-go/v7/pkg/credentials" "github.com/minio/minio/internal/auth" xioutil "github.com/minio/minio/internal/ioutil" - "github.com/minio/pkg/v3/mimedb" + "github.com/pgsty/silo-pkg/v3/mimedb" ftp "goftp.io/server/v2" ) diff --git a/cmd/generic-handlers.go b/cmd/generic-handlers.go index 88a111668..0b3156d8b 100644 --- a/cmd/generic-handlers.go +++ b/cmd/generic-handlers.go @@ -33,7 +33,7 @@ import ( "github.com/minio/minio-go/v7/pkg/s3utils" "github.com/minio/minio-go/v7/pkg/set" "github.com/minio/minio/internal/grid" - xnet "github.com/minio/pkg/v3/net" + xnet "github.com/pgsty/silo-pkg/v3/net" "github.com/minio/minio/internal/amztime" "github.com/minio/minio/internal/config/dns" @@ -476,9 +476,10 @@ func setRequestValidityMiddleware(h http.Handler) http.Handler { // is obtained from centralized etcd configuration service. func setBucketForwardingMiddleware(h http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if origin := w.Header().Get("Access-Control-Allow-Origin"); origin == "null" { + if origin := w.Header().Get("Access-Control-Allow-Origin"); origin == "null" && !bucketCorsWasApplied(r) { // This is a workaround change to ensure that "Origin: null" - // incoming request to a response back as "*" instead of "null" + // incoming request to a response back as "*" instead of "null". + // Per-bucket CORS preserves an explicitly allowed "null" origin. w.Header().Set("Access-Control-Allow-Origin", "*") } if globalDNSConfig == nil || !globalBucketFederation || diff --git a/cmd/global-heal.go b/cmd/global-heal.go index 57cce16ee..22bf54ca6 100644 --- a/cmd/global-heal.go +++ b/cmd/global-heal.go @@ -35,9 +35,9 @@ import ( "github.com/minio/minio/internal/color" "github.com/minio/minio/internal/config/storageclass" "github.com/minio/minio/internal/logger" - "github.com/minio/pkg/v3/console" - "github.com/minio/pkg/v3/wildcard" - "github.com/minio/pkg/v3/workers" + "github.com/pgsty/silo-pkg/v3/console" + "github.com/pgsty/silo-pkg/v3/wildcard" + "github.com/pgsty/silo-pkg/v3/workers" ) const ( diff --git a/cmd/globals.go b/cmd/globals.go index f70413a69..ebec422ae 100644 --- a/cmd/globals.go +++ b/cmd/globals.go @@ -55,9 +55,9 @@ import ( levent "github.com/minio/minio/internal/config/lambda/event" "github.com/minio/minio/internal/event" "github.com/minio/minio/internal/pubsub" - "github.com/minio/pkg/v3/certs" - "github.com/minio/pkg/v3/env" - xnet "github.com/minio/pkg/v3/net" + "github.com/pgsty/silo-pkg/v3/certs" + "github.com/pgsty/silo-pkg/v3/env" + xnet "github.com/pgsty/silo-pkg/v3/net" ) // minio configuration related constants. diff --git a/cmd/handler-utils.go b/cmd/handler-utils.go index 1e26d897d..0569f02b3 100644 --- a/cmd/handler-utils.go +++ b/cmd/handler-utils.go @@ -33,7 +33,7 @@ import ( xhttp "github.com/minio/minio/internal/http" "github.com/minio/minio/internal/logger" "github.com/minio/minio/internal/mcontext" - xnet "github.com/minio/pkg/v3/net" + xnet "github.com/pgsty/silo-pkg/v3/net" ) const ( @@ -83,7 +83,6 @@ var supportedHeaders = []string{ xhttp.AmzStorageClass, xhttp.AmzObjectTagging, "expires", - xhttp.AmzBucketReplicationStatus, "X-Minio-Replication-Server-Side-Encryption-Sealed-Key", "X-Minio-Replication-Server-Side-Encryption-Seal-Algorithm", "X-Minio-Replication-Server-Side-Encryption-Iv", @@ -332,7 +331,7 @@ func extractReqParams(r *http.Request) map[string]string { m["range"] = rangeField } - if _, ok := r.Header[xhttp.MinIOSourceReplicationRequest]; ok { + if isTrustedReplication(r.Context()) { m[xhttp.MinIOSourceReplicationRequest] = "" } return m diff --git a/cmd/handler-utils_test.go b/cmd/handler-utils_test.go index 6a7bcd611..15c206dc4 100644 --- a/cmd/handler-utils_test.go +++ b/cmd/handler-utils_test.go @@ -307,7 +307,7 @@ func TestGetCopyObjectMetadataFromHeaderReplication(t *testing.T) { } } -func TestCloneRequestWithoutCopyReplicationHeaders(t *testing.T) { +func TestCloneRequestWithoutReplicationHeaders(t *testing.T) { req, err := http.NewRequest(http.MethodPut, "http://localhost/test", nil) if err != nil { t.Fatal(err) @@ -320,9 +320,12 @@ func TestCloneRequestWithoutCopyReplicationHeaders(t *testing.T) { req.Header.Set(xhttp.MinIOSourceObjectLegalHoldTimestamp, "2026-04-15T10:00:00Z") req.Header.Set(xhttp.MinIOReplicationActualObjectSize, "123") req.Header.Set(ReplicationSsecChecksumHeader, "checksum") + req.Header.Set(xhttp.AmzBucketReplicationStatus, "REPLICA") + req.Header.Set(xhttp.MinIOSourceDeleteMarker, "true") + req.Header.Set("X-Minio-Replication-Server-Side-Encryption-Sealed-Key", "sealed") req.Header.Set("Content-Type", "application/octet-stream") - clone := cloneRequestWithoutCopyReplicationHeaders(req) + clone := cloneRequestWithoutReplicationHeaders(t.Context(), req) if clone == req { t.Fatal("expected cloned request") } @@ -336,6 +339,9 @@ func TestCloneRequestWithoutCopyReplicationHeaders(t *testing.T) { xhttp.MinIOSourceObjectLegalHoldTimestamp, xhttp.MinIOReplicationActualObjectSize, ReplicationSsecChecksumHeader, + xhttp.AmzBucketReplicationStatus, + xhttp.MinIOSourceDeleteMarker, + "X-Minio-Replication-Server-Side-Encryption-Sealed-Key", } { if got := clone.Header.Get(header); got != "" { t.Fatalf("expected %s to be stripped, got %q", header, got) diff --git a/cmd/iam-object-store.go b/cmd/iam-object-store.go index 7da764577..b1865878f 100644 --- a/cmd/iam-object-store.go +++ b/cmd/iam-object-store.go @@ -35,7 +35,7 @@ import ( xioutil "github.com/minio/minio/internal/ioutil" "github.com/minio/minio/internal/kms" "github.com/minio/minio/internal/logger" - "github.com/minio/pkg/v3/sync/errgroup" + "github.com/pgsty/silo-pkg/v3/sync/errgroup" "github.com/puzpuzpuz/xsync/v3" ) diff --git a/cmd/iam-store.go b/cmd/iam-store.go index 46465e00b..ec992b5d0 100644 --- a/cmd/iam-store.go +++ b/cmd/iam-store.go @@ -37,9 +37,9 @@ import ( "github.com/minio/minio/internal/config" "github.com/minio/minio/internal/config/identity/openid" "github.com/minio/minio/internal/jwt" - "github.com/minio/pkg/v3/env" - "github.com/minio/pkg/v3/policy" - "github.com/minio/pkg/v3/sync/errgroup" + "github.com/pgsty/silo-pkg/v3/env" + "github.com/pgsty/silo-pkg/v3/policy" + "github.com/pgsty/silo-pkg/v3/sync/errgroup" "github.com/puzpuzpuz/xsync/v3" "golang.org/x/sync/singleflight" ) diff --git a/cmd/iam.go b/cmd/iam.go index 3dc86ccf8..f27a20062 100644 --- a/cmd/iam.go +++ b/cmd/iam.go @@ -48,9 +48,9 @@ import ( xhttp "github.com/minio/minio/internal/http" "github.com/minio/minio/internal/jwt" "github.com/minio/minio/internal/logger" - "github.com/minio/pkg/v3/env" - "github.com/minio/pkg/v3/ldap" - "github.com/minio/pkg/v3/policy" + "github.com/pgsty/silo-pkg/v3/env" + "github.com/pgsty/silo-pkg/v3/ldap" + "github.com/pgsty/silo-pkg/v3/policy" etcd "go.etcd.io/etcd/client/v3" "golang.org/x/sync/singleflight" ) diff --git a/cmd/jwt.go b/cmd/jwt.go index c86b8f676..d8acbe7f0 100644 --- a/cmd/jwt.go +++ b/cmd/jwt.go @@ -27,7 +27,7 @@ import ( jwtreq "github.com/golang-jwt/jwt/v4/request" "github.com/minio/minio/internal/auth" xjwt "github.com/minio/minio/internal/jwt" - "github.com/minio/pkg/v3/policy" + "github.com/pgsty/silo-pkg/v3/policy" ) const ( diff --git a/cmd/kms-handlers.go b/cmd/kms-handlers.go index ce5017c1f..fb73c5935 100644 --- a/cmd/kms-handlers.go +++ b/cmd/kms-handlers.go @@ -26,7 +26,7 @@ import ( "github.com/minio/minio/internal/auth" "github.com/minio/minio/internal/kms" "github.com/minio/minio/internal/logger" - "github.com/minio/pkg/v3/policy" + "github.com/pgsty/silo-pkg/v3/policy" ) // KMSStatusHandler - GET /minio/kms/v1/status diff --git a/cmd/kms-handlers_test.go b/cmd/kms-handlers_test.go index 4eccab4cd..7aa5b7c2b 100644 --- a/cmd/kms-handlers_test.go +++ b/cmd/kms-handlers_test.go @@ -28,7 +28,7 @@ import ( "github.com/minio/madmin-go/v3" "github.com/minio/minio/internal/kms" - "github.com/minio/pkg/v3/policy" + "github.com/pgsty/silo-pkg/v3/policy" ) const ( diff --git a/cmd/listen-notification-handlers.go b/cmd/listen-notification-handlers.go index 9f3210daf..d2a957ec2 100644 --- a/cmd/listen-notification-handlers.go +++ b/cmd/listen-notification-handlers.go @@ -30,7 +30,7 @@ import ( "github.com/minio/minio/internal/logger" "github.com/minio/minio/internal/pubsub" "github.com/minio/mux" - "github.com/minio/pkg/v3/policy" + "github.com/pgsty/silo-pkg/v3/policy" ) func (api objectAPIHandlers) ListenNotificationHandler(w http.ResponseWriter, r *http.Request) { diff --git a/cmd/main.go b/cmd/main.go index fad6c8483..3e11d70bb 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -30,10 +30,10 @@ import ( "github.com/minio/cli" "github.com/minio/minio/internal/color" "github.com/minio/minio/internal/logger" - "github.com/minio/pkg/v3/console" - "github.com/minio/pkg/v3/env" - "github.com/minio/pkg/v3/trie" - "github.com/minio/pkg/v3/words" + "github.com/pgsty/silo-pkg/v3/console" + "github.com/pgsty/silo-pkg/v3/env" + "github.com/pgsty/silo-pkg/v3/trie" + "github.com/pgsty/silo-pkg/v3/words" ) // GlobalFlags - global flags for minio. diff --git a/cmd/metacache-bucket.go b/cmd/metacache-bucket.go index 4df23d825..249e54122 100644 --- a/cmd/metacache-bucket.go +++ b/cmd/metacache-bucket.go @@ -27,7 +27,7 @@ import ( "time" "github.com/minio/minio/internal/logger" - "github.com/minio/pkg/v3/console" + "github.com/pgsty/silo-pkg/v3/console" ) // a bucketMetacache keeps track of all caches generated diff --git a/cmd/metacache-entries.go b/cmd/metacache-entries.go index 69c5e835c..fceec0b43 100644 --- a/cmd/metacache-entries.go +++ b/cmd/metacache-entries.go @@ -26,7 +26,7 @@ import ( "strings" xioutil "github.com/minio/minio/internal/ioutil" - "github.com/minio/pkg/v3/console" + "github.com/pgsty/silo-pkg/v3/console" ) // metaCacheEntry is an object or a directory within an unknown bucket. diff --git a/cmd/metacache-server-pool.go b/cmd/metacache-server-pool.go index bcdfed8f1..c62f822ce 100644 --- a/cmd/metacache-server-pool.go +++ b/cmd/metacache-server-pool.go @@ -71,13 +71,13 @@ func (z *erasureServerPools) listPath(ctx context.Context, o *listPathOptions) ( if o.Marker != "" && o.Prefix != "" { // Marker not common with prefix is not implemented. Send an empty response if !HasPrefix(o.Marker, o.Prefix) { - return entries, io.EOF + return entries, z.listPathShortcutEOF(ctx, o.Bucket) } } // With max keys of zero we have reached eof, return right here. if o.Limit == 0 { - return entries, io.EOF + return entries, z.listPathShortcutEOF(ctx, o.Bucket) } // For delimiter and prefix as '/' we do not list anything at all @@ -85,7 +85,7 @@ func (z *erasureServerPools) listPath(ctx context.Context, o *listPathOptions) ( // as '/' we don't have any entries, since all the keys are // of form 'keyName/...' if strings.HasPrefix(o.Prefix, SlashSeparator) { - return entries, io.EOF + return entries, z.listPathShortcutEOF(ctx, o.Bucket) } // If delimiter is slashSeparator we must return directories of @@ -254,6 +254,16 @@ func (z *erasureServerPools) listPath(ctx context.Context, o *listPathOptions) ( return entries, nil } +// listPathShortcutEOF verifies bucket existence for shortcuts that return +// without consulting the storage layer. Keep this check out of the normal +// listing path since GetBucketInfo fans out to peers and disks. +func (z *erasureServerPools) listPathShortcutEOF(ctx context.Context, bucket string) error { + if _, err := z.GetBucketInfo(ctx, bucket, BucketOptions{}); err != nil { + return err + } + return io.EOF +} + // listMerged will list across all sets and return a merged results stream. // The result channel is closed when no more results are expected. func (z *erasureServerPools) listMerged(ctx context.Context, o listPathOptions, results chan<- metaCacheEntry) error { diff --git a/cmd/metacache-set.go b/cmd/metacache-set.go index c43d18d71..a2ab603c6 100644 --- a/cmd/metacache-set.go +++ b/cmd/metacache-set.go @@ -40,7 +40,7 @@ import ( "github.com/minio/minio/internal/color" "github.com/minio/minio/internal/hash" xioutil "github.com/minio/minio/internal/ioutil" - "github.com/minio/pkg/v3/console" + "github.com/pgsty/silo-pkg/v3/console" ) //go:generate msgp -file $GOFILE -unexported diff --git a/cmd/metacache.go b/cmd/metacache.go index 7f35d391f..30a97670b 100644 --- a/cmd/metacache.go +++ b/cmd/metacache.go @@ -25,7 +25,7 @@ import ( "strings" "time" - "github.com/minio/pkg/v3/console" + "github.com/pgsty/silo-pkg/v3/console" ) type scanStatus uint8 diff --git a/cmd/metrics-realtime.go b/cmd/metrics-realtime.go index 4a1d55732..9eb10990f 100644 --- a/cmd/metrics-realtime.go +++ b/cmd/metrics-realtime.go @@ -191,7 +191,9 @@ func collectLocalDisksMetrics(disks map[string]struct{}) map[string]madmin.DiskM } } + //nolint:staticcheck // Linux implementations can fail; BSD stubs return a constant nil error. st, err := disk.GetDriveStats(d.Major, d.Minor) + //nolint:staticcheck // Keep the shared cross-platform error handling. if err == nil { dm.IOStats = madmin.DiskIOStats{ ReadIOs: st.ReadIOs, diff --git a/cmd/metrics-router.go b/cmd/metrics-router.go index f8b85c254..0f3cdbebd 100644 --- a/cmd/metrics-router.go +++ b/cmd/metrics-router.go @@ -22,7 +22,7 @@ import ( "strings" "github.com/minio/mux" - "github.com/minio/pkg/v3/env" + "github.com/pgsty/silo-pkg/v3/env" ) const ( diff --git a/cmd/metrics-v2.go b/cmd/metrics-v2.go index e38750a58..b8d5c1290 100644 --- a/cmd/metrics-v2.go +++ b/cmd/metrics-v2.go @@ -3314,10 +3314,10 @@ func getBucketUsageMetrics(opts MetricsGroupOpts) *MetricsGroupV2 { VariableLabels: map[string]string{"bucket": bucket}, }) - if quota != nil && quota.Quota > 0 { + if quotaSize := getBucketQuotaSize(quota); quotaSize > 0 { metrics = append(metrics, MetricV2{ Description: getBucketUsageQuotaTotalBytesMD(), - Value: float64(quota.Quota), + Value: float64(quotaSize), VariableLabels: map[string]string{"bucket": bucket}, }) } diff --git a/cmd/metrics-v3-cluster-usage.go b/cmd/metrics-v3-cluster-usage.go index 38dc0aef3..3d05be951 100644 --- a/cmd/metrics-v3-cluster-usage.go +++ b/cmd/metrics-v3-cluster-usage.go @@ -167,8 +167,8 @@ func loadClusterUsageBucketMetrics(ctx context.Context, m MetricValues, c *metri m.Set(usageBucketVersionsCount, float64(usage.VersionsCount), "bucket", bucket) m.Set(usageBucketDeleteMarkersCount, float64(usage.DeleteMarkersCount), "bucket", bucket) - if quota != nil && quota.Quota > 0 { - m.Set(usageBucketQuotaTotalBytes, float64(quota.Quota), "bucket", bucket) + if quotaSize := getBucketQuotaSize(quota); quotaSize > 0 { + m.Set(usageBucketQuotaTotalBytes, float64(quotaSize), "bucket", bucket) } for k, v := range usage.ObjectSizesHistogram { diff --git a/cmd/metrics-v3-handler.go b/cmd/metrics-v3-handler.go index 7f07f58ef..2332f3e58 100644 --- a/cmd/metrics-v3-handler.go +++ b/cmd/metrics-v3-handler.go @@ -28,7 +28,7 @@ import ( "github.com/minio/minio/internal/config" "github.com/minio/minio/internal/mcontext" "github.com/minio/mux" - "github.com/minio/pkg/v3/env" + "github.com/pgsty/silo-pkg/v3/env" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promhttp" ) diff --git a/cmd/metrics.go b/cmd/metrics.go index c5dc3cdfd..97f7a85f8 100644 --- a/cmd/metrics.go +++ b/cmd/metrics.go @@ -24,7 +24,7 @@ import ( "github.com/minio/minio/internal/auth" "github.com/minio/minio/internal/logger" "github.com/minio/minio/internal/mcontext" - "github.com/minio/pkg/v3/policy" + "github.com/pgsty/silo-pkg/v3/policy" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/common/expfmt" ) diff --git a/cmd/mrf.go b/cmd/mrf.go index 4d002c27a..31f99c45b 100644 --- a/cmd/mrf.go +++ b/cmd/mrf.go @@ -31,7 +31,7 @@ import ( "github.com/google/uuid" "github.com/minio/madmin-go/v3" - "github.com/minio/pkg/v3/wildcard" + "github.com/pgsty/silo-pkg/v3/wildcard" "github.com/tinylib/msgp/msgp" ) diff --git a/cmd/net.go b/cmd/net.go index f0462851b..94fe8ddcf 100644 --- a/cmd/net.go +++ b/cmd/net.go @@ -28,7 +28,7 @@ import ( "github.com/minio/minio-go/v7/pkg/set" "github.com/minio/minio/internal/config" "github.com/minio/minio/internal/logger" - xnet "github.com/minio/pkg/v3/net" + xnet "github.com/pgsty/silo-pkg/v3/net" ) var ( diff --git a/cmd/notification.go b/cmd/notification.go index 152856ac2..4b9ad3ffb 100644 --- a/cmd/notification.go +++ b/cmd/notification.go @@ -34,9 +34,9 @@ import ( "github.com/klauspost/compress/zip" "github.com/minio/madmin-go/v3" xioutil "github.com/minio/minio/internal/ioutil" - xnet "github.com/minio/pkg/v3/net" - "github.com/minio/pkg/v3/sync/errgroup" - "github.com/minio/pkg/v3/workers" + xnet "github.com/pgsty/silo-pkg/v3/net" + "github.com/pgsty/silo-pkg/v3/sync/errgroup" + "github.com/pgsty/silo-pkg/v3/workers" "github.com/minio/minio/internal/bucket/bandwidth" "github.com/minio/minio/internal/logger" diff --git a/cmd/object-api-errors.go b/cmd/object-api-errors.go index ef3723938..b4915b258 100644 --- a/cmd/object-api-errors.go +++ b/cmd/object-api-errors.go @@ -22,6 +22,7 @@ import ( "errors" "fmt" "io" + "strings" ) // Converts underlying storage error. Convenience function written to @@ -653,6 +654,41 @@ func (e InvalidPart) Error() string { e.PartNumber, e.ExpETag, e.GotETag) } +var ( + errCompleteMultipartChecksumMismatch = errors.New("complete multipart checksum mismatch") + errCompleteMultipartChecksumTypeMismatch = errors.New("complete multipart checksum type mismatch") + errMissingPartChecksum = errors.New("missing multipart part checksum") +) + +// completeMultipartChecksumMismatch reports an object checksum mismatch +// detected while completing a multipart upload. It is distinct from +// hash.ChecksumMismatch because AWS maps completion failures to BadDigest, +// while streaming PutObject and UploadPart failures keep using +// XAmzContentChecksumMismatch. +func completeMultipartChecksumMismatch(algorithm string) error { + description := "The checksum you specified did not match the calculated checksum." + if algorithm != "" { + description = fmt.Sprintf("The %s checksum you specified did not match the calculated checksum.", algorithm) + } + return fmt.Errorf("%w: %s", errCompleteMultipartChecksumMismatch, description) +} + +// completeMultipartChecksumTypeMismatch reports an explicit checksum type that +// differs from the type selected when the multipart upload was initiated. +func completeMultipartChecksumTypeMismatch(providedType, expectedType string) error { + description := fmt.Sprintf("The checksum type %s does not match the multipart upload checksum type %s.", + providedType, expectedType) + return fmt.Errorf("%w: %s", errCompleteMultipartChecksumTypeMismatch, description) +} + +// missingPartChecksum reports a part whose checksum is absent from a +// composite CompleteMultipartUpload request. +func missingPartChecksum(algorithm string, partNumber int) error { + description := fmt.Sprintf("The upload was created using a %s checksum. The complete request must include the checksum for each part. It was missing for part %d in the request.", + strings.ToLower(algorithm), partNumber) + return fmt.Errorf("%w: %s", errMissingPartChecksum, description) +} + // PartTooSmall - error if part size is less than 5MB. type PartTooSmall struct { PartSize int64 diff --git a/cmd/object-api-interface.go b/cmd/object-api-interface.go index 565b10fd1..68c9e230f 100644 --- a/cmd/object-api-interface.go +++ b/cmd/object-api-interface.go @@ -84,7 +84,8 @@ type ObjectOptions struct { Expiration ExpirationOptions LifecycleAuditEvent lcAuditEvent - WantChecksum *hash.Checksum // x-amz-checksum-XXX checksum sent to PutObject/ CompleteMultipartUpload. + WantChecksum *hash.Checksum // x-amz-checksum-XXX checksum sent to PutObject/ CompleteMultipartUpload. + wantChecksumType string // explicit x-amz-checksum-type value on CompleteMultipartUpload. WantServerSideChecksumType hash.ChecksumType // if set, we compute a server-side checksum of this type @@ -98,6 +99,7 @@ type ObjectOptions struct { ReplicationSourceTaggingTimestamp time.Time // set if MinIOSourceTaggingTimestamp received ReplicationSourceLegalholdTimestamp time.Time // set if MinIOSourceObjectLegalholdTimestamp received ReplicationSourceRetentionTimestamp time.Time // set if MinIOSourceObjectRetentionTimestamp received + ReplicaLockReconcile bool // set for a trusted SSE-C replica full write/completion: re-order Object Lock against the destination version read under the write lock (single erasure set; see pgsty/silo#133) DeletePrefix bool // set true to enforce a prefix deletion, only application for DeleteObject API, DeletePrefixObject bool // set true when object's erasure set is resolvable by object name (using getHashedSetIndex) diff --git a/cmd/object-api-options.go b/cmd/object-api-options.go index 6482f2000..7c454873f 100644 --- a/cmd/object-api-options.go +++ b/cmd/object-api-options.go @@ -41,9 +41,6 @@ func getDefaultOpts(header http.Header, copySource bool, metadata map[string]str opts.ProxyHeaderSet = true opts.ProxyRequest = strings.Join(v, "") == "true" } - if _, ok := header[xhttp.MinIOSourceReplicationRequest]; ok { - opts.ReplicationRequest = true - } opts.Speedtest = header.Get(globalObjectPerfUserMetadata) != "" if copySource { @@ -116,12 +113,15 @@ func getOpts(ctx context.Context, r *http.Request, bucket, object string) (Objec } opts.PartNumber = partNumber opts.VersionID = vid + opts.ReplicationRequest = isTrustedReplication(ctx) - delMarker, err := parseBoolHeader(bucket, object, r.Header, xhttp.MinIOSourceDeleteMarker) - if err != nil { - return opts, err + if opts.ReplicationRequest { + delMarker, err := parseBoolHeader(bucket, object, r.Header, xhttp.MinIOSourceDeleteMarker) + if err != nil { + return opts, err + } + opts.DeleteMarker = delMarker } - opts.DeleteMarker = delMarker replReadyCheck, err := parseBoolHeader(bucket, object, r.Header, xhttp.MinIOCheckDMReplicationReady) if err != nil { @@ -184,6 +184,16 @@ func getAndValidateAttributesOpts(ctx context.Context, w http.ResponseWriter, r return opts, valid } + // Reject out-of-range page sizes as ListObjectParts does, instead of + // answering an invalid request with an empty parts listing. + if opts.MaxParts < 0 { + apiErr = errorCodes.ToAPIErr(ErrInvalidMaxParts) + argumentName = strings.ToLower(xhttp.AmzMaxParts) + argumentValue = r.Header.Get(xhttp.AmzMaxParts) + valid = false + return opts, valid + } + if opts.MaxParts == 0 { opts.MaxParts = maxPartsList } @@ -196,6 +206,14 @@ func getAndValidateAttributesOpts(ctx context.Context, w http.ResponseWriter, r return opts, valid } + if opts.PartNumberMarker < 0 { + apiErr = errorCodes.ToAPIErr(ErrInvalidPartNumberMarker) + argumentName = strings.ToLower(xhttp.AmzPartNumberMarker) + argumentValue = r.Header.Get(xhttp.AmzPartNumberMarker) + valid = false + return opts, valid + } + opts.ObjectAttributes = parseObjectAttributes(r.Header) if len(opts.ObjectAttributes) < 1 { apiErr = errorCodes.ToAPIErr(ErrInvalidAttributeName) @@ -297,20 +315,22 @@ func delOpts(ctx context.Context, r *http.Request, bucket, object string) (opts opts.VersionID = nullVersionID } - delMarker, err := parseBoolHeader(bucket, object, r.Header, xhttp.MinIOSourceDeleteMarker) - if err != nil { - return opts, err - } - opts.DeleteMarker = delMarker - - mtime := strings.TrimSpace(r.Header.Get(xhttp.MinIOSourceMTime)) - if mtime != "" { - opts.MTime, err = time.Parse(time.RFC3339Nano, mtime) + if isTrustedReplication(ctx) { + delMarker, err := parseBoolHeader(bucket, object, r.Header, xhttp.MinIOSourceDeleteMarker) if err != nil { - return opts, InvalidArgument{ - Bucket: bucket, - Object: object, - Err: fmt.Errorf("Unable to parse %s, failed with %w", xhttp.MinIOSourceMTime, err), + return opts, err + } + opts.DeleteMarker = delMarker + + mtime := strings.TrimSpace(r.Header.Get(xhttp.MinIOSourceMTime)) + if mtime != "" { + opts.MTime, err = time.Parse(time.RFC3339Nano, mtime) + if err != nil { + return opts, InvalidArgument{ + Bucket: bucket, + Object: object, + Err: fmt.Errorf("Unable to parse %s, failed with %w", xhttp.MinIOSourceMTime, err), + } } } } @@ -319,10 +339,10 @@ func delOpts(ctx context.Context, r *http.Request, bucket, object string) (opts // get ObjectOptions for PUT calls from encryption headers and metadata func putOptsFromReq(ctx context.Context, r *http.Request, bucket, object string, metadata map[string]string) (opts ObjectOptions, err error) { - return putOpts(ctx, bucket, object, r.Form.Get(xhttp.VersionID), r.Header, metadata) + return putOpts(ctx, bucket, object, r.Form.Get(xhttp.VersionID), r.Header, metadata, isTrustedReplication(ctx)) } -func putOpts(ctx context.Context, bucket, object, vid string, hdrs http.Header, metadata map[string]string) (opts ObjectOptions, err error) { +func putOpts(ctx context.Context, bucket, object, vid string, hdrs http.Header, metadata map[string]string, trustedReplication bool) (opts ObjectOptions, err error) { versioned := globalBucketVersioningSys.PrefixEnabled(bucket, object) versionSuspended := globalBucketVersioningSys.PrefixSuspended(bucket, object) @@ -344,7 +364,7 @@ func putOpts(ctx context.Context, bucket, object, vid string, hdrs http.Header, } } } - opts, err = putOptsFromHeaders(ctx, hdrs, metadata) + opts, err = putOptsFromHeaders(ctx, hdrs, metadata, trustedReplication) if err != nil { return opts, InvalidArgument{ Bucket: bucket, @@ -365,8 +385,15 @@ func putOpts(ctx context.Context, bucket, object, vid string, hdrs http.Header, return opts, nil } -func putOptsFromHeaders(ctx context.Context, hdr http.Header, metadata map[string]string) (opts ObjectOptions, err error) { - mtimeStr := strings.TrimSpace(hdr.Get(xhttp.MinIOSourceMTime)) +func putOptsFromHeaders(ctx context.Context, hdr http.Header, metadata map[string]string, trustedReplication bool) (opts ObjectOptions, err error) { + var mtimeStr, retaintimeStr, lholdtimeStr, tagtimeStr, etag string + if trustedReplication { + mtimeStr = strings.TrimSpace(hdr.Get(xhttp.MinIOSourceMTime)) + retaintimeStr = strings.TrimSpace(hdr.Get(xhttp.MinIOSourceObjectRetentionTimestamp)) + lholdtimeStr = strings.TrimSpace(hdr.Get(xhttp.MinIOSourceObjectLegalHoldTimestamp)) + tagtimeStr = strings.TrimSpace(hdr.Get(xhttp.MinIOSourceTaggingTimestamp)) + etag = strings.TrimSpace(hdr.Get(xhttp.MinIOSourceETag)) + } var mtime time.Time if mtimeStr != "" { mtime, err = time.Parse(time.RFC3339Nano, mtimeStr) @@ -374,7 +401,6 @@ func putOptsFromHeaders(ctx context.Context, hdr http.Header, metadata map[strin return opts, fmt.Errorf("Unable to parse %s, failed with %w", xhttp.MinIOSourceMTime, err) } } - retaintimeStr := strings.TrimSpace(hdr.Get(xhttp.MinIOSourceObjectRetentionTimestamp)) var retaintimestmp time.Time if retaintimeStr != "" { retaintimestmp, err = time.Parse(time.RFC3339, retaintimeStr) @@ -383,7 +409,6 @@ func putOptsFromHeaders(ctx context.Context, hdr http.Header, metadata map[strin } } - lholdtimeStr := strings.TrimSpace(hdr.Get(xhttp.MinIOSourceObjectLegalHoldTimestamp)) var lholdtimestmp time.Time if lholdtimeStr != "" { lholdtimestmp, err = time.Parse(time.RFC3339, lholdtimeStr) @@ -391,7 +416,6 @@ func putOptsFromHeaders(ctx context.Context, hdr http.Header, metadata map[strin return opts, fmt.Errorf("Unable to parse %s, failed with %w", xhttp.MinIOSourceObjectLegalHoldTimestamp, err) } } - tagtimeStr := strings.TrimSpace(hdr.Get(xhttp.MinIOSourceTaggingTimestamp)) var taggingtimestmp time.Time if tagtimeStr != "" { taggingtimestmp, err = time.Parse(time.RFC3339, tagtimeStr) @@ -404,7 +428,6 @@ func putOptsFromHeaders(ctx context.Context, hdr http.Header, metadata map[strin metadata = make(map[string]string) } - etag := strings.TrimSpace(hdr.Get(xhttp.MinIOSourceETag)) if crypto.S3KMS.IsRequested(hdr) { keyID, context, err := crypto.S3KMS.ParseHTTP(hdr) if err != nil { @@ -419,6 +442,12 @@ func putOptsFromHeaders(ctx context.Context, hdr http.Header, metadata map[strin UserDefined: metadata, MTime: mtime, PreserveETag: etag, + ReplicationRequest: trustedReplication, + // The Object Lock timestamps order replicated retention and legal + // hold updates. Dropping them here would leave every update on an + // SSE-KMS destination unordered. + ReplicationSourceLegalholdTimestamp: lholdtimestmp, + ReplicationSourceRetentionTimestamp: retaintimestmp, } return op, nil } @@ -429,6 +458,7 @@ func putOptsFromHeaders(ctx context.Context, hdr http.Header, metadata map[strin } opts.MTime = mtime + opts.ReplicationRequest = trustedReplication opts.ReplicationSourceLegalholdTimestamp = lholdtimestmp opts.ReplicationSourceRetentionTimestamp = retaintimestmp opts.ReplicationSourceTaggingTimestamp = taggingtimestmp @@ -439,6 +469,9 @@ func putOptsFromHeaders(ctx context.Context, hdr http.Header, metadata map[strin // get ObjectOptions for Copy calls with encryption headers provided on the target side and source side metadata func copyDstOpts(ctx context.Context, r *http.Request, bucket, object string, metadata map[string]string) (opts ObjectOptions, err error) { + if _, err := hash.GetContentChecksum(r.Header); err != nil { + return opts, err + } return putOptsFromReq(ctx, r, bucket, object, metadata) } @@ -451,12 +484,17 @@ func copySrcOpts(ctx context.Context, r *http.Request, bucket, object string) (O if err != nil { return opts, err } + opts.ReplicationRequest = isReplicaTrusted(ctx) return opts, nil } // get ObjectOptions for CompleteMultipart calls func completeMultipartOpts(ctx context.Context, r *http.Request, bucket, object string) (opts ObjectOptions, err error) { - mtimeStr := strings.TrimSpace(r.Header.Get(xhttp.MinIOSourceMTime)) + trustedReplication := isTrustedReplication(ctx) + var mtimeStr string + if trustedReplication { + mtimeStr = strings.TrimSpace(r.Header.Get(xhttp.MinIOSourceMTime)) + } var mtime time.Time if mtimeStr != "" { mtime, err = time.Parse(time.RFC3339Nano, mtimeStr) @@ -469,6 +507,13 @@ func completeMultipartOpts(ctx context.Context, r *http.Request, bucket, object } } + opts.wantChecksumType = r.Header.Get(xhttp.AmzChecksumType) + switch opts.wantChecksumType { + case "", xhttp.AmzChecksumTypeComposite, xhttp.AmzChecksumTypeFullObject: + default: + return opts, hash.ErrInvalidChecksum + } + opts.WantChecksum, err = hash.GetContentChecksum(r.Header) if err != nil { return opts, err @@ -485,12 +530,12 @@ func completeMultipartOpts(ctx context.Context, r *http.Request, bucket, object } } } - if _, ok := r.Header[xhttp.MinIOSourceReplicationRequest]; ok { + if trustedReplication { opts.ReplicationRequest = true opts.UserDefined[ReservedMetadataPrefix+"Actual-Object-Size"] = r.Header.Get(xhttp.MinIOReplicationActualObjectSize) - } - if r.Header.Get(ReplicationSsecChecksumHeader) != "" { - opts.UserDefined[ReplicationSsecChecksumHeader] = r.Header.Get(ReplicationSsecChecksumHeader) + if r.Header.Get(ReplicationSsecChecksumHeader) != "" { + opts.UserDefined[ReplicationSsecChecksumHeader] = r.Header.Get(ReplicationSsecChecksumHeader) + } } return opts, nil } diff --git a/cmd/object-api-options_test.go b/cmd/object-api-options_test.go index 661372cd4..2c229f8ff 100644 --- a/cmd/object-api-options_test.go +++ b/cmd/object-api-options_test.go @@ -18,9 +18,11 @@ package cmd import ( + "encoding/xml" "net/http" "net/http/httptest" "reflect" + "strings" "testing" xhttp "github.com/minio/minio/internal/http" @@ -76,3 +78,109 @@ func TestGetAndValidateAttributesOpts(t *testing.T) { }) } } + +// TestGetAndValidateAttributesOptsPartsRange asserts that GetObjectAttributes +// range checks the ObjectParts pagination headers the way ListObjectParts +// does: a negative value is rejected with the same API error, an absent or +// zero x-amz-max-parts means the default page size, and in-range values are +// passed through unchanged. +func TestGetAndValidateAttributesOptsPartsRange(t *testing.T) { + globalBucketVersioningSys = &BucketVersioningSys{} + bucket := minioMetaBucket + ctx := t.Context() + + testCases := []struct { + name string + maxParts string + marker string + wantValid bool + wantErr APIErrorCode + wantArgument string + wantValue string + wantMaxParts int + wantMarker int + }{ + { + name: "defaults", + wantValid: true, + wantMaxParts: maxPartsList, + }, + { + name: "zero max-parts means the default page size", + maxParts: "0", + wantValid: true, + wantMaxParts: maxPartsList, + }, + { + name: "in range values pass through", + maxParts: "10", + marker: "3", + wantValid: true, + wantMaxParts: 10, + wantMarker: 3, + }, + { + name: "negative max-parts is rejected", + maxParts: "-1", + wantValid: false, + wantErr: ErrInvalidMaxParts, + wantArgument: strings.ToLower(xhttp.AmzMaxParts), + wantValue: "-1", + }, + { + name: "negative part-number-marker is rejected", + marker: "-1", + wantValid: false, + wantErr: ErrInvalidPartNumberMarker, + wantArgument: strings.ToLower(xhttp.AmzPartNumberMarker), + wantValue: "-1", + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + rec := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodGet, "/testbucket/testobject?attributes", nil) + req.Header.Set(xhttp.AmzObjectAttributes, "ObjectParts") + if testCase.maxParts != "" { + req.Header.Set(xhttp.AmzMaxParts, testCase.maxParts) + } + if testCase.marker != "" { + req.Header.Set(xhttp.AmzPartNumberMarker, testCase.marker) + } + + opts, valid := getAndValidateAttributesOpts(ctx, rec, req, bucket, "testobject") + if valid != testCase.wantValid { + t.Fatalf("want valid %v, got %v (%s)", testCase.wantValid, valid, rec.Body.String()) + } + + if testCase.wantValid { + if opts.MaxParts != testCase.wantMaxParts { + t.Errorf("want MaxParts %d, got %d", testCase.wantMaxParts, opts.MaxParts) + } + if opts.PartNumberMarker != testCase.wantMarker { + t.Errorf("want PartNumberMarker %d, got %d", testCase.wantMarker, opts.PartNumberMarker) + } + return + } + + wantErr := errorCodes.ToAPIErr(testCase.wantErr) + if rec.Code != wantErr.HTTPStatusCode { + t.Errorf("want HTTP status %d, got %d", wantErr.HTTPStatusCode, rec.Code) + } + var errResp objectAttributesErrorResponse + if err := xml.Unmarshal(rec.Body.Bytes(), &errResp); err != nil { + t.Fatalf("decode error response: %v (%s)", err, rec.Body.String()) + } + if errResp.Code != wantErr.Code || errResp.Message != wantErr.Description { + t.Errorf("want error %s/%q, got %s/%q", wantErr.Code, wantErr.Description, errResp.Code, errResp.Message) + } + if errResp.ArgumentName == nil || *errResp.ArgumentName != testCase.wantArgument { + t.Errorf("want ArgumentName %q, got %v", testCase.wantArgument, errResp.ArgumentName) + } + if errResp.ArgumentValue == nil || *errResp.ArgumentValue != testCase.wantValue { + t.Errorf("want ArgumentValue %q, got %v", testCase.wantValue, errResp.ArgumentValue) + } + }) + } +} diff --git a/cmd/object-api-utils.go b/cmd/object-api-utils.go index 5d791ce45..e8648b725 100644 --- a/cmd/object-api-utils.go +++ b/cmd/object-api-utils.go @@ -49,8 +49,9 @@ import ( xhttp "github.com/minio/minio/internal/http" xioutil "github.com/minio/minio/internal/ioutil" "github.com/minio/minio/internal/logger" - "github.com/minio/pkg/v3/trie" - "github.com/minio/pkg/v3/wildcard" + "github.com/minio/sio" + "github.com/pgsty/silo-pkg/v3/trie" + "github.com/pgsty/silo-pkg/v3/wildcard" "github.com/valyala/bytebufferpool" ) @@ -610,7 +611,10 @@ func excludeForCompression(header http.Header, object string, cfg compress.Confi return true } - if crypto.Requested(header) && !cfg.AllowEncrypted { + // SSE-C replication sends raw ciphertext without compression metadata. + // Exclude new SSE-C data from compression; other modes follow allow_encryption. + if crypto.SSEC.IsRequested(header) || + (crypto.Requested(header) && !cfg.AllowEncrypted) { return true } @@ -675,19 +679,35 @@ func getPartFile(entriesTrie *trie.Trie, partNumber int, etag string) (partFile return partFile } -func partNumberToRangeSpec(oi ObjectInfo, partNumber int) *HTTPRangeSpec { +func partNumberToRangeSpec(oi ObjectInfo, partNumber int) (*HTTPRangeSpec, error) { if oi.Size == 0 || len(oi.Parts) == 0 { - return nil + return nil, nil } + // For an encrypted, uncompressed object derive each part's plaintext length + // from the stored ciphertext length instead of trusting ActualSize: parts + // written before this was normalised record the ciphertext length there. + // The range returned here is consumed in the plaintext domain, where + // GetDecryptedRange and DecryptedSize both use exactly this arithmetic. + _, isEncrypted := crypto.IsEncrypted(oi.UserDefined) + deriveFromSize := isEncrypted && !oi.IsCompressed() + var start int64 end := int64(-1) for i := 0; i < len(oi.Parts) && i < partNumber; i++ { + partSize := oi.Parts[i].ActualSize + if deriveFromSize { + decrypted, err := sio.DecryptedSize(uint64(oi.Parts[i].Size)) + if err != nil { + return nil, errObjectTampered + } + partSize = int64(decrypted) + } start = end + 1 - end = start + oi.Parts[i].ActualSize - 1 + end = start + partSize - 1 } - return &HTTPRangeSpec{Start: start, End: end} + return &HTTPRangeSpec{Start: start, End: end}, nil } // Returns the compressed offset which should be skipped. @@ -806,7 +826,10 @@ func NewGetObjectReader(rs *HTTPRangeSpec, oi ObjectInfo, opts ObjectOptions, h } if rs == nil && opts.PartNumber > 0 { - rs = partNumberToRangeSpec(oi, opts.PartNumber) + rs, err = partNumberToRangeSpec(oi, opts.PartNumber) + if err != nil { + return nil, 0, 0, err + } } _, isEncrypted := crypto.IsEncrypted(oi.UserDefined) @@ -1038,9 +1061,10 @@ type SealMD5CurrFn func([]byte) []byte // PutObjReader is a type that wraps sio.EncryptReader and // underlying hash.Reader in a struct type PutObjReader struct { - *hash.Reader // actual data stream - rawReader *hash.Reader // original data stream - sealMD5Fn SealMD5CurrFn + *hash.Reader // actual data stream + rawReader *hash.Reader // original data stream used for ETag calculation + checksumReader *hash.Reader // logical plaintext stream used for S3 checksum calculation + sealMD5Fn SealMD5CurrFn } // Size returns the absolute number of bytes the Reader @@ -1093,15 +1117,51 @@ func (p *PutObjReader) WithEncryption(encReader *hash.Reader, objEncKey *crypto. // NewPutObjReader returns a new PutObjReader. It uses given hash.Reader's // MD5Current method to construct md5sum when requested downstream. func NewPutObjReader(rawReader *hash.Reader) *PutObjReader { - return &PutObjReader{Reader: rawReader, rawReader: rawReader} + return &PutObjReader{Reader: rawReader, rawReader: rawReader, checksumReader: rawReader} +} + +// setChecksumReader sets the logical plaintext reader used for S3 checksums. +// It can differ from rawReader when the storage stream is compressed. +func (p *PutObjReader) setChecksumReader(r *hash.Reader) { + if r != nil { + p.checksumReader = r + } +} + +// contentChecksumType returns the effective client-provided or server-computed +// checksum type for the logical plaintext stream. +func (p *PutObjReader) contentChecksumType() hash.ChecksumType { + if p.checksumReader == nil { + return hash.ChecksumNone + } + if t := p.checksumReader.ContentCRCType(); t.IsSet() { + return t + } + return p.checksumReader.ServerSideChecksumType +} + +// contentChecksum returns the effective checksum for part metadata. A +// client-provided checksum takes precedence; server computation is only a +// fallback when the client omitted one. +func (p *PutObjReader) contentChecksum() map[string]string { + if p.checksumReader == nil { + return nil + } + if checksum := p.checksumReader.ContentCRC(); checksum != nil { + return checksum + } + if checksum := p.checksumReader.ServerSideChecksumResult; checksum != nil && checksum.Valid() { + return map[string]string{checksum.Type.String(): checksum.Encoded} + } + return nil } // RawServerSideChecksumResult returns the ServerSideChecksumResult from the -// underlying rawReader, since the PutObjReader might be encrypted data and -// thus any checksum from that would be incorrect. +// logical plaintext checksum reader, since the PutObjReader might contain +// compressed or encrypted data and thus any checksum from that would be incorrect. func (p *PutObjReader) RawServerSideChecksumResult() *hash.Checksum { - if p.rawReader != nil { - return p.rawReader.ServerSideChecksumResult + if p.checksumReader != nil { + return p.checksumReader.ServerSideChecksumResult } return nil } diff --git a/cmd/object-api-utils_test.go b/cmd/object-api-utils_test.go index 53992bf86..dd20a8821 100644 --- a/cmd/object-api-utils_test.go +++ b/cmd/object-api-utils_test.go @@ -36,7 +36,7 @@ import ( "github.com/minio/minio/internal/auth" "github.com/minio/minio/internal/config/compress" "github.com/minio/minio/internal/crypto" - "github.com/minio/pkg/v3/trie" + "github.com/pgsty/silo-pkg/v3/trie" ) func pathJoinOld(elem ...string) string { diff --git a/cmd/object-attributes-parts-pagination_test.go b/cmd/object-attributes-parts-pagination_test.go new file mode 100644 index 000000000..c80a0327d --- /dev/null +++ b/cmd/object-attributes-parts-pagination_test.go @@ -0,0 +1,185 @@ +// Copyright (c) 2015-2026 MinIO, Inc. +// +// This file is part of MinIO Object Storage stack +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package cmd + +import ( + "bytes" + "encoding/xml" + "fmt" + "net/http" + "net/http/httptest" + "strconv" + "testing" + + "github.com/minio/minio/internal/auth" + xhttp "github.com/minio/minio/internal/http" +) + +type attributesPartsPage struct { + ObjectParts struct { + IsTruncated bool + MaxParts int + NextPartNumberMarker int + PartNumberMarker int + PartsCount int + Parts []struct { + PartNumber int + Size int64 + } `xml:"Part"` + } +} + +// TestAPIGetObjectAttributesPartsPagination asserts that ObjectParts pagination +// terminates for sparse part numbers: truncation is decided by whether parts +// remain, not by comparing the last part number with the part count. +func TestAPIGetObjectAttributesPartsPagination(t *testing.T) { + defer DetectTestLeak(t)() + ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{ + t: t, + objAPITest: testAPIGetObjectAttributesPartsPagination, + }) +} + +func testAPIGetObjectAttributesPartsPagination(_ ObjectLayer, instanceType, bucketName string, + apiRouter http.Handler, credentials auth.Credentials, t *testing.T, +) { + signedRequest := func(method, target string, body []byte, headers map[string]string) *httptest.ResponseRecorder { + t.Helper() + req, err := newTestSignedRequestV4(method, target, int64(len(body)), bytes.NewReader(body), + credentials.AccessKey, credentials.SecretKey, headers) + if err != nil { + t.Fatal(err) + } + rec := httptest.NewRecorder() + apiRouter.ServeHTTP(rec, req) + return rec + } + + const partSize = 5 * 1024 * 1024 + + // upload completes a multipart object with the given part numbers. Part + // numbers must be strictly increasing, but gaps are allowed, so the last + // part number need not equal the number of parts. + upload := func(object string, partNumbers []int) { + t.Helper() + initRec := signedRequest(http.MethodPost, getNewMultipartURL("", bucketName, object), nil, nil) + if initRec.Code != http.StatusOK { + t.Fatalf("%s INIT: %d %s", instanceType, initRec.Code, initRec.Body.String()) + } + var initiated struct { + UploadID string `xml:"UploadId"` + } + if err := xml.Unmarshal(initRec.Body.Bytes(), &initiated); err != nil { + t.Fatal(err) + } + var complete bytes.Buffer + complete.WriteString("") + for i, number := range partNumbers { + body := bytes.Repeat([]byte("abcd"), partSize/4) + if i == len(partNumbers)-1 { + body = bytes.Repeat([]byte("12345"), 103) + } + put := signedRequest(http.MethodPut, + getPutObjectPartURL("", bucketName, object, initiated.UploadID, strconv.Itoa(number)), body, nil) + if put.Code != http.StatusOK { + t.Fatalf("%s PART %d: %d %s", instanceType, number, put.Code, put.Body.String()) + } + fmt.Fprintf(&complete, "%d%s", + number, put.Header()[xhttp.ETag][0]) + } + complete.WriteString("") + finish := signedRequest(http.MethodPost, + getCompleteMultipartUploadURL("", bucketName, object, initiated.UploadID), complete.Bytes(), nil) + if finish.Code != http.StatusOK { + t.Fatalf("%s COMPLETE: %d %s", instanceType, finish.Code, finish.Body.String()) + } + } + + attributes := func(object string, marker, maxParts int) attributesPartsPage { + t.Helper() + headers := map[string]string{xhttp.AmzObjectAttributes: "ObjectParts"} + if marker > 0 { + headers[xhttp.AmzPartNumberMarker] = strconv.Itoa(marker) + } + if maxParts > 0 { + headers[xhttp.AmzMaxParts] = strconv.Itoa(maxParts) + } + rec := signedRequest(http.MethodGet, getGetObjectURL("", bucketName, object)+"?attributes", nil, headers) + if rec.Code != http.StatusOK { + t.Fatalf("%s ATTRIBUTES marker=%d max=%d: %d %s", instanceType, marker, maxParts, rec.Code, rec.Body.String()) + } + var page attributesPartsPage + if err := xml.Unmarshal(rec.Body.Bytes(), &page); err != nil { + t.Fatal(err) + } + return page + } + + check := func(name string, page attributesPartsPage, wantParts []int, wantTruncated bool, wantNext, wantCount int) { + t.Helper() + got := make([]int, 0, len(page.ObjectParts.Parts)) + for _, p := range page.ObjectParts.Parts { + got = append(got, p.PartNumber) + } + if fmt.Sprint(got) != fmt.Sprint(wantParts) { + t.Errorf("%s %s: parts=%v want=%v", instanceType, name, got, wantParts) + } + if page.ObjectParts.IsTruncated != wantTruncated { + t.Errorf("%s %s: IsTruncated=%v want=%v", instanceType, name, page.ObjectParts.IsTruncated, wantTruncated) + } + if page.ObjectParts.NextPartNumberMarker != wantNext { + t.Errorf("%s %s: NextPartNumberMarker=%d want=%d", instanceType, name, + page.ObjectParts.NextPartNumberMarker, wantNext) + } + if page.ObjectParts.PartsCount != wantCount { + t.Errorf("%s %s: PartsCount=%d want=%d", instanceType, name, page.ObjectParts.PartsCount, wantCount) + } + } + + sparse := "review/attributes-pagination-sparse" + upload(sparse, []int{1, 3, 5}) + + // A single page holding every sparse part is complete. + check("sparse full page", attributes(sparse, 0, 0), []int{1, 3, 5}, false, 0, 3) + + // One part per page walks 1, 3, 5 and stops. Page 2 returns part 3, + // whose number equals the part count, and must still be truncated: + // the old comparison declared that page final and silently dropped + // part 5. + check("sparse max-parts=1 page 1", attributes(sparse, 0, 1), []int{1}, true, 1, 3) + check("sparse max-parts=1 page 2", attributes(sparse, 1, 1), []int{3}, true, 3, 3) + check("sparse max-parts=1 page 3", attributes(sparse, 3, 1), []int{5}, false, 0, 3) + + // A page that exactly holds the remainder is not truncated, so no + // empty trailing page is requested. + check("sparse max-parts=2 page 2", attributes(sparse, 1, 2), []int{3, 5}, false, 0, 3) + + // A marker at or past the last part ends the listing instead of + // looping back through NextPartNumberMarker=0. + check("sparse marker at last part", attributes(sparse, 5, 0), nil, false, 0, 3) + check("sparse marker past last part", attributes(sparse, 9, 0), nil, false, 0, 3) + + // Contiguous numbering is affected too: the empty page past the end + // used to report IsTruncated=true with NextPartNumberMarker=0. + contiguous := "review/attributes-pagination-contiguous" + upload(contiguous, []int{1, 2}) + check("contiguous full page", attributes(contiguous, 0, 0), []int{1, 2}, false, 0, 2) + check("contiguous max-parts=1 page 1", attributes(contiguous, 0, 1), []int{1}, true, 1, 2) + check("contiguous max-parts=1 page 2", attributes(contiguous, 1, 1), []int{2}, false, 0, 2) + check("contiguous marker at last part", attributes(contiguous, 2, 0), nil, false, 0, 2) +} diff --git a/cmd/object-attributes-parts_test.go b/cmd/object-attributes-parts_test.go new file mode 100644 index 000000000..5c2ea02b2 --- /dev/null +++ b/cmd/object-attributes-parts_test.go @@ -0,0 +1,410 @@ +// Copyright (c) 2015-2026 MinIO, Inc. +// Copyright (c) 2026 PGSTY +// +// This file is part of MinIO Object Storage stack +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package cmd + +import ( + "bytes" + "context" + "crypto/md5" + "encoding/base64" + "encoding/xml" + "fmt" + "maps" + "net/http" + "net/http/httptest" + "strconv" + "testing" + + "github.com/minio/minio/internal/auth" + "github.com/minio/minio/internal/crypto" + xhttp "github.com/minio/minio/internal/http" +) + +// attributesPartsResponse is the subset of the GetObjectAttributes response +// the ObjectParts tests assert on. +type attributesPartsResponse struct { + ObjectSize int64 + ObjectParts struct { + IsTruncated bool + NextPartNumberMarker int + PartsCount int + Parts []struct { + PartNumber int + Size int64 + } `xml:"Part"` + } +} + +func attributesPartsSSECHeaders(key []byte) map[string]string { + digest := md5.Sum(key) + return map[string]string{ + xhttp.AmzServerSideEncryptionCustomerAlgorithm: xhttp.AmzEncryptionAES, + xhttp.AmzServerSideEncryptionCustomerKey: base64.StdEncoding.EncodeToString(key), + xhttp.AmzServerSideEncryptionCustomerKeyMD5: base64.StdEncoding.EncodeToString(digest[:]), + } +} + +func attributesPartsSignedRequest(t *testing.T, apiRouter http.Handler, credentials auth.Credentials, + method, target string, body []byte, headers map[string]string, +) *httptest.ResponseRecorder { + t.Helper() + req, err := newTestSignedRequestV4(method, target, int64(len(body)), bytes.NewReader(body), + credentials.AccessKey, credentials.SecretKey, headers) + if err != nil { + t.Fatal(err) + } + rec := httptest.NewRecorder() + apiRouter.ServeHTTP(rec, req) + return rec +} + +// attributesPartsUpload completes a multipart upload of bodies under +// partNumbers and returns the concatenated plaintext. +func attributesPartsUpload(t *testing.T, apiRouter http.Handler, credentials auth.Credentials, + bucketName, object string, headers map[string]string, bodies [][]byte, partNumbers []int, +) []byte { + t.Helper() + initRec := attributesPartsSignedRequest(t, apiRouter, credentials, http.MethodPost, + getNewMultipartURL("", bucketName, object), nil, headers) + if initRec.Code != http.StatusOK { + t.Fatalf("NewMultipart %s: %d %s", object, initRec.Code, initRec.Body.String()) + } + var initiated struct { + UploadID string `xml:"UploadId"` + } + if err := xml.Unmarshal(initRec.Body.Bytes(), &initiated); err != nil { + t.Fatal(err) + } + + var complete bytes.Buffer + complete.WriteString("") + var data []byte + for i, body := range bodies { + data = append(data, body...) + put := attributesPartsSignedRequest(t, apiRouter, credentials, http.MethodPut, + getPutObjectPartURL("", bucketName, object, initiated.UploadID, strconv.Itoa(partNumbers[i])), body, headers) + if put.Code != http.StatusOK { + t.Fatalf("PutObjectPart %s part %d: %d %s", object, partNumbers[i], put.Code, put.Body.String()) + } + fmt.Fprintf(&complete, "%d%s", + partNumbers[i], put.Header()[xhttp.ETag][0]) + } + complete.WriteString("") + + finish := attributesPartsSignedRequest(t, apiRouter, credentials, http.MethodPost, + getCompleteMultipartUploadURL("", bucketName, object, initiated.UploadID), complete.Bytes(), headers) + if finish.Code != http.StatusOK { + t.Fatalf("CompleteMultipartUpload %s: %d %s", object, finish.Code, finish.Body.String()) + } + return data +} + +// attributesPartsFetch issues GetObjectAttributes for ObjectSize and +// ObjectParts and decodes the response. +func attributesPartsFetch(t *testing.T, apiRouter http.Handler, credentials auth.Credentials, + bucketName, object string, headers map[string]string, +) attributesPartsResponse { + t.Helper() + attributeHeaders := maps.Clone(headers) + if attributeHeaders == nil { + attributeHeaders = make(map[string]string) + } + attributeHeaders[xhttp.AmzObjectAttributes] = "ObjectSize,ObjectParts" + rec := attributesPartsSignedRequest(t, apiRouter, credentials, http.MethodGet, + getGetObjectURL("", bucketName, object)+"?attributes", nil, attributeHeaders) + if rec.Code != http.StatusOK { + t.Fatalf("GetObjectAttributes %s: %d %s", object, rec.Code, rec.Body.String()) + } + var response attributesPartsResponse + if err := xml.Unmarshal(rec.Body.Bytes(), &response); err != nil { + t.Fatalf("decode GetObjectAttributes response: %v (%s)", err, rec.Body.String()) + } + return response +} + +// enableAttributesPartsCompression turns on compression for ".txt" objects, +// including encrypted ones, for the duration of the test. +func enableAttributesPartsCompression(t *testing.T) { + t.Helper() + globalCompressConfigMu.Lock() + previous := globalCompressConfig + globalCompressConfig.Enabled = true + globalCompressConfig.Extensions = []string{".txt"} + globalCompressConfig.MimeTypes = nil + globalCompressConfig.AllowEncrypted = true + globalCompressConfigMu.Unlock() + t.Cleanup(func() { + globalCompressConfigMu.Lock() + globalCompressConfig = previous + globalCompressConfigMu.Unlock() + }) +} + +// TestAPIGetObjectAttributesMultipartLogicalPartSize asserts that ObjectPart.Size +// reports the uploaded plaintext length of every part, not the transformed +// length stored on disk. Compressed parts must report the pre-compression +// length and encrypted parts the pre-encryption length, for consecutive as +// well as sparse part numbering. +func TestAPIGetObjectAttributesMultipartLogicalPartSize(t *testing.T) { + defer DetectTestLeak(t)() + ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{ + t: t, + objAPITest: testAPIGetObjectAttributesMultipartLogicalPartSize, + }) +} + +func testAPIGetObjectAttributesMultipartLogicalPartSize(_ ObjectLayer, instanceType, bucketName string, + apiRouter http.Handler, credentials auth.Credentials, t *testing.T, +) { + previousTLS := globalIsTLS + globalIsTLS = true + defer func() { globalIsTLS = previousTLS }() + enableAttributesPartsCompression(t) + + for _, variant := range []struct { + name string + extension string + encrypted bool + }{ + {name: "plain", extension: ".bin"}, + {name: "compressed", extension: ".txt"}, + {name: "ssec", extension: ".bin", encrypted: true}, + {name: "compressed-ssec", extension: ".txt", encrypted: true}, + } { + for _, numbering := range []struct { + name string + partNumbers []int + }{ + {name: "consecutive", partNumbers: []int{1, 2}}, + {name: "sparse", partNumbers: []int{1, 3}}, + } { + t.Run(variant.name+"/"+numbering.name, func(t *testing.T) { + var headers map[string]string + if variant.encrypted { + headers = attributesPartsSSECHeaders(bytes.Repeat([]byte{0x19}, 32)) + } + object := "attributes/parts-" + variant.name + "-" + numbering.name + variant.extension + bodies := [][]byte{ + bytes.Repeat([]byte("abcd"), 5*1024*1024/4), + bytes.Repeat([]byte("12345"), 103), + } + data := attributesPartsUpload(t, apiRouter, credentials, bucketName, object, headers, bodies, numbering.partNumbers) + + get := attributesPartsSignedRequest(t, apiRouter, credentials, http.MethodGet, + getGetObjectURL("", bucketName, object), nil, headers) + if get.Code != http.StatusOK || !bytes.Equal(get.Body.Bytes(), data) { + t.Fatalf("%s GET: %d bytes=%d want=%d", instanceType, get.Code, get.Body.Len(), len(data)) + } + + response := attributesPartsFetch(t, apiRouter, credentials, bucketName, object, headers) + if response.ObjectSize != int64(len(data)) { + t.Errorf("%s ObjectSize=%d want=%d", instanceType, response.ObjectSize, len(data)) + } + if len(response.ObjectParts.Parts) != len(bodies) { + t.Fatalf("%s part count=%d want=%d", instanceType, len(response.ObjectParts.Parts), len(bodies)) + } + if response.ObjectParts.IsTruncated { + t.Errorf("%s lists all %d parts %v, but IsTruncated=true NextPartNumberMarker=%d", + instanceType, len(bodies), numbering.partNumbers, response.ObjectParts.NextPartNumberMarker) + } + var total int64 + for i, part := range response.ObjectParts.Parts { + if part.PartNumber != numbering.partNumbers[i] { + t.Errorf("%s part %d number=%d want=%d", instanceType, i, part.PartNumber, numbering.partNumbers[i]) + } + if part.Size != int64(len(bodies[i])) { + t.Errorf("%s part %d size=%d want logical size=%d", + instanceType, part.PartNumber, part.Size, len(bodies[i])) + } + total += part.Size + } + if total != response.ObjectSize { + t.Errorf("%s part sizes sum to %d, ObjectSize=%d", instanceType, total, response.ObjectSize) + } + }) + } + } +} + +// TestAPIGetObjectAttributesCompressedEmptyTrailingPart pins the reported +// size of a compressed part carrying no payload. Such a part stores +// Size 0 and ActualSize 0, so it exercises the lower bound of the +// ActualSize guard and must keep reporting 0. +func TestAPIGetObjectAttributesCompressedEmptyTrailingPart(t *testing.T) { + defer DetectTestLeak(t)() + ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{ + t: t, + objAPITest: testAPIGetObjectAttributesCompressedEmptyTrailingPart, + }) +} + +func testAPIGetObjectAttributesCompressedEmptyTrailingPart(_ ObjectLayer, instanceType, bucketName string, + apiRouter http.Handler, credentials auth.Credentials, t *testing.T, +) { + enableAttributesPartsCompression(t) + + object := "attributes/parts-compressed-empty-tail.txt" + bodies := [][]byte{bytes.Repeat([]byte("abcd"), 5*1024*1024/4), {}} + data := attributesPartsUpload(t, apiRouter, credentials, bucketName, object, nil, bodies, []int{1, 2}) + + response := attributesPartsFetch(t, apiRouter, credentials, bucketName, object, nil) + if response.ObjectSize != int64(len(data)) { + t.Errorf("%s ObjectSize=%d want=%d", instanceType, response.ObjectSize, len(data)) + } + if len(response.ObjectParts.Parts) != len(bodies) { + t.Fatalf("%s part count=%d want=%d", instanceType, len(response.ObjectParts.Parts), len(bodies)) + } + for i, part := range response.ObjectParts.Parts { + if part.Size != int64(len(bodies[i])) { + t.Errorf("%s part %d size=%d want logical size=%d", + instanceType, part.PartNumber, part.Size, len(bodies[i])) + } + } +} + +// TestAPIGetObjectAttributesEncryptedPartLengths pins how a part length that +// cannot be a valid encrypted stream is reported, for both encrypted layouts. +// Parts of an encrypted multipart object are separate streams, so an +// unconvertible one is corrupt and must fail the request. DecryptObjectInfo +// does not catch that: ObjectInfo.isMultipart gives up on the first part that +// fails sio.DecryptedSize, after which ObjectInfo.DecryptedSize validates only +// the object total. A legacy encrypted object carries no multipart marker and +// is one continuous stream that the erasure writer split into storage +// fragments; those fragments are not independently decryptable, so they must +// keep their stored size rather than fail an intact object. Both fixtures use +// part lengths 5245473 and 1, whose sum 5245474 is a valid stream length while +// the second part alone is not. +func TestAPIGetObjectAttributesEncryptedPartLengths(t *testing.T) { + defer DetectTestLeak(t)() + ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{ + t: t, + objAPITest: testAPIGetObjectAttributesEncryptedPartLengths, + }) +} + +func testAPIGetObjectAttributesEncryptedPartLengths(obj ObjectLayer, instanceType, bucketName string, + apiRouter http.Handler, credentials auth.Credentials, t *testing.T, +) { + // Part lengths 5245473 and 1: the sum 5245474 is a valid encrypted stream + // length while the second part alone is not. Since pgsty/silo#119, + // PutObjectPart derives an encrypted part's plaintext length from the bytes + // written and rejects one that cannot be a valid stream, so an object with + // these per-part sizes can no longer be created through a normal write; it + // only exists as pre-#119 on-disk state or from an old peer. Inject that + // stored shape directly through the object layer and exercise the handler, + // which is what this test pins. + partLengths := []int64{5245473, 1} + + for _, variant := range []struct { + name string + metadata map[string]string + tampered bool + }{ + { + // Parts of an encrypted multipart object are separate streams, so an + // unconvertible one is corrupt and must fail the request. + name: "separately-encrypted-parts", + metadata: map[string]string{crypto.MetaMultipart: ""}, + tampered: true, + }, + { + // A legacy encrypted object carries no multipart marker and is one + // continuous stream split into storage fragments that are not + // independently decryptable, so they keep their stored size. + name: "legacy-single-stream", + metadata: map[string]string{crypto.MetaIV: "legacy"}, + }, + } { + t.Run(variant.name, func(t *testing.T) { + object := "attributes/parts-encrypted-" + variant.name + var total int64 + infoParts := make([]ObjectPartInfo, 0, len(partLengths)) + for i, length := range partLengths { + total += length + infoParts = append(infoParts, ObjectPartInfo{Number: i + 1, Size: length, ActualSize: length}) + } + info := ObjectInfo{ + Bucket: bucketName, + Name: object, + Size: total, + ModTime: UTCNow(), + IsLatest: true, + UserDefined: maps.Clone(variant.metadata), + Parts: infoParts, + } + + previous := newObjectLayerFn() + setObjectLayer(&attributesPartsObjectLayer{ObjectLayer: obj, bucket: bucketName, object: object, info: info}) + defer setObjectLayer(previous) + + rec := attributesPartsSignedRequest(t, apiRouter, credentials, http.MethodGet, + getGetObjectURL("", bucketName, object)+"?attributes", nil, + map[string]string{xhttp.AmzObjectAttributes: "ObjectParts"}) + + if variant.tampered { + wantErr := errorCodes.ToAPIErr(ErrObjectTampered) + if rec.Code != wantErr.HTTPStatusCode { + t.Fatalf("%s status %d, want %d: %s", instanceType, rec.Code, wantErr.HTTPStatusCode, rec.Body.String()) + } + var errResp APIErrorResponse + if err := xml.Unmarshal(rec.Body.Bytes(), &errResp); err != nil { + t.Fatalf("decode error response: %v (%s)", err, rec.Body.String()) + } + if errResp.Code != wantErr.Code { + t.Errorf("%s error code %q, want %q", instanceType, errResp.Code, wantErr.Code) + } + return + } + + if rec.Code != http.StatusOK { + t.Fatalf("%s status %d, want %d: %s", instanceType, rec.Code, http.StatusOK, rec.Body.String()) + } + var response attributesPartsResponse + if err := xml.Unmarshal(rec.Body.Bytes(), &response); err != nil { + t.Fatalf("decode GetObjectAttributes response: %v (%s)", err, rec.Body.String()) + } + if len(response.ObjectParts.Parts) != len(partLengths) { + t.Fatalf("%s part count=%d want=%d", instanceType, len(response.ObjectParts.Parts), len(partLengths)) + } + for i, part := range response.ObjectParts.Parts { + if part.Size != partLengths[i] { + t.Errorf("%s part %d size=%d, want the stored fragment size %d", + instanceType, part.PartNumber, part.Size, partLengths[i]) + } + } + }) + } +} + +// attributesPartsObjectLayer returns a crafted ObjectInfo for one object so a +// stored shape that pgsty/silo#119 no longer lets PutObjectPart create can be +// handed to the GetObjectAttributes handler under test; every other call falls +// through to the real layer. +type attributesPartsObjectLayer struct { + ObjectLayer + bucket, object string + info ObjectInfo +} + +func (o *attributesPartsObjectLayer) GetObjectInfo(ctx context.Context, bucket, object string, opts ObjectOptions) (ObjectInfo, error) { + if bucket == o.bucket && object == o.object { + return o.info.Clone(), nil + } + return o.ObjectLayer.GetObjectInfo(ctx, bucket, object, opts) +} diff --git a/cmd/object-attributes-ssec-authz_test.go b/cmd/object-attributes-ssec-authz_test.go new file mode 100644 index 000000000..63bd515bb --- /dev/null +++ b/cmd/object-attributes-ssec-authz_test.go @@ -0,0 +1,139 @@ +// Copyright (c) 2015-2025 MinIO, Inc. +// Copyright (c) 2025-2026 PGSTY +// +// This file is part of MinIO Object Storage stack +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package cmd + +import ( + "bytes" + "crypto/md5" + "encoding/base64" + "net/http" + "strings" + "testing" + + "github.com/minio/madmin-go/v3" + "github.com/minio/minio/internal/auth" + xhttp "github.com/minio/minio/internal/http" + "github.com/pgsty/silo-pkg/v3/policy" +) + +// GetObjectAttributes lets a replication peer read SSE-C attributes without +// presenting the customer key. The X-Minio-Source-Replication-Request header +// that marks such a request is client controlled, so the carve-out has to be +// gated on the caller actually holding s3:ReplicateObject. Root credentials +// hold every action, so a root-only test cannot tell the gate apart from an +// ungated header check; these cases drive it with least-privilege users. +func TestAPIGetObjectAttributesSSECReplicationAuthz(t *testing.T) { + defer DetectTestLeak(t)() + ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{ + t: t, + objAPITest: testAPIGetObjectAttributesSSECReplicationAuthz, + }) +} + +func testAPIGetObjectAttributesSSECReplicationAuthz(_ ObjectLayer, instanceType, bucketName string, + apiRouter http.Handler, credentials auth.Credentials, t *testing.T, +) { + previousTLS := globalIsTLS + globalIsTLS = true + defer func() { globalIsTLS = previousTLS }() + + key := bytes.Repeat([]byte{0x11}, 32) + keyMD5 := md5.Sum(key) + sseHeaders := map[string]string{ + xhttp.AmzServerSideEncryptionCustomerAlgorithm: xhttp.AmzEncryptionAES, + xhttp.AmzServerSideEncryptionCustomerKey: base64.StdEncoding.EncodeToString(key), + xhttp.AmzServerSideEncryptionCustomerKeyMD5: base64.StdEncoding.EncodeToString(keyMD5[:]), + } + replicationHeader := map[string]string{xhttp.MinIOSourceReplicationRequest: "true"} + + object := "attributes/ssec-replication-authz" + putCopyChecksumSource(t, apiRouter, credentials, bucketName, object, + bytes.Repeat([]byte("attributes-authz-"), 512), sseHeaders) + + // s3:GetObjectAttributes and s3:GetObject reach the handler; only the + // second policy adds the replication action the carve-out requires. + readerOnly := newObjectAttributesAuthzUser(t, instanceType, bucketName, + `"s3:GetObject","s3:GetObjectAttributes"`) + replicator := newObjectAttributesAuthzUser(t, instanceType, bucketName, + `"s3:GetObject","s3:GetObjectAttributes","s3:ReplicateObject"`) + + for _, test := range []struct { + name string + creds auth.Credentials + headers map[string]string + want int + }{ + // The header alone must not stand in for s3:ReplicateObject. + {name: "reader/replication-header-only", creds: readerOnly, headers: replicationHeader, want: http.StatusBadRequest}, + // A caller that may replicate this object keeps the carve-out. + {name: "replicator/replication-header-only", creds: replicator, headers: replicationHeader, want: http.StatusOK}, + // Root holds s3:ReplicateObject, so its carve-out is unchanged. + {name: "root/replication-header-only", creds: credentials, headers: replicationHeader, want: http.StatusOK}, + // The ordinary key-bearing path is unaffected for every caller. + {name: "reader/correct-key", creds: readerOnly, headers: sseHeaders, want: http.StatusOK}, + {name: "reader/no-key", creds: readerOnly, want: http.StatusBadRequest}, + } { + t.Run(test.name, func(t *testing.T) { + rec := objectAttributesSSECRequest(t, apiRouter, test.creds, bucketName, object, test.headers) + if rec.Code != test.want { + t.Fatalf("%s: status %d, want %d: %s", instanceType, rec.Code, test.want, rec.Body.String()) + } + }) + } +} + +// newObjectAttributesAuthzUser installs a least-privilege user whose policy +// grants exactly the listed actions on bucketName's objects. +func newObjectAttributesAuthzUser(t *testing.T, instanceType, bucketName, actions string) auth.Credentials { + t.Helper() + ctx := t.Context() + + accessKey, secretKey, err := auth.GenerateCredentials() + if err != nil { + t.Fatalf("%s: generate credentials: %v", instanceType, err) + } + creds := auth.Credentials{AccessKey: accessKey, SecretKey: secretKey} + if _, err = globalIAMSys.CreateUser(ctx, creds.AccessKey, madmin.AddOrUpdateUserReq{ + SecretKey: creds.SecretKey, + Status: madmin.AccountEnabled, + }); err != nil { + t.Fatalf("%s: create attributes user: %v", instanceType, err) + } + + policyJSON := `{ + "Version": "2012-10-17", + "Statement": [{ + "Effect": "Allow", + "Action": [` + actions + `], + "Resource": ["arn:aws:s3:::` + bucketName + `/*"] + }] +}` + parsed, err := policy.ParseConfig(strings.NewReader(policyJSON)) + if err != nil { + t.Fatalf("%s: parse attributes policy: %v", instanceType, err) + } + policyName := "attributes-authz-" + mustGetUUID() + if _, err = globalIAMSys.SetPolicy(ctx, policyName, *parsed); err != nil { + t.Fatalf("%s: install attributes policy: %v", instanceType, err) + } + if _, err = globalIAMSys.PolicyDBSet(ctx, creds.AccessKey, policyName, regUser, false); err != nil { + t.Fatalf("%s: attach attributes policy: %v", instanceType, err) + } + return creds +} diff --git a/cmd/object-attributes-ssec_test.go b/cmd/object-attributes-ssec_test.go new file mode 100644 index 000000000..77deca059 --- /dev/null +++ b/cmd/object-attributes-ssec_test.go @@ -0,0 +1,104 @@ +// Copyright (c) 2015-2026 MinIO, Inc. +// +// This file is part of MinIO Object Storage stack +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package cmd + +import ( + "bytes" + "crypto/md5" + "encoding/base64" + "net/http" + "net/http/httptest" + "testing" + + "github.com/minio/minio/internal/auth" + xhttp "github.com/minio/minio/internal/http" +) + +func TestAPIGetObjectAttributesAuthenticatesSSECKey(t *testing.T) { + defer DetectTestLeak(t)() + ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{ + t: t, + objAPITest: testAPIGetObjectAttributesAuthenticatesSSECKey, + }) +} + +func testAPIGetObjectAttributesAuthenticatesSSECKey(_ ObjectLayer, instanceType, bucketName string, + apiRouter http.Handler, credentials auth.Credentials, t *testing.T, +) { + previousTLS := globalIsTLS + globalIsTLS = true + defer func() { globalIsTLS = previousTLS }() + + key := bytes.Repeat([]byte{0x11}, 32) + keyMD5 := md5.Sum(key) + wrongKey := bytes.Repeat([]byte{0x22}, 32) + wrongMD5 := md5.Sum(wrongKey) + correctHeaders := map[string]string{ + xhttp.AmzServerSideEncryptionCustomerAlgorithm: xhttp.AmzEncryptionAES, + xhttp.AmzServerSideEncryptionCustomerKey: base64.StdEncoding.EncodeToString(key), + xhttp.AmzServerSideEncryptionCustomerKeyMD5: base64.StdEncoding.EncodeToString(keyMD5[:]), + } + wrongHeaders := map[string]string{ + xhttp.AmzServerSideEncryptionCustomerAlgorithm: xhttp.AmzEncryptionAES, + xhttp.AmzServerSideEncryptionCustomerKey: base64.StdEncoding.EncodeToString(wrongKey), + xhttp.AmzServerSideEncryptionCustomerKeyMD5: base64.StdEncoding.EncodeToString(wrongMD5[:]), + } + + for _, test := range []struct { + name string + data []byte + }{ + {name: "zero", data: nil}, + {name: "nonzero", data: []byte("secret")}, + } { + object := "attributes/ssec-" + test.name + putCopyChecksumSource(t, apiRouter, credentials, bucketName, object, test.data, correctHeaders) + if rec := objectAttributesSSECRequest(t, apiRouter, credentials, bucketName, object, correctHeaders); rec.Code != http.StatusOK { + t.Fatalf("%s/%s: correct key returned %d: %s", instanceType, test.name, rec.Code, rec.Body.String()) + } + if rec := objectAttributesSSECRequest(t, apiRouter, credentials, bucketName, object, wrongHeaders); rec.Code != http.StatusForbidden { + t.Fatalf("%s/%s: wrong key returned %d, want %d: %s", instanceType, test.name, rec.Code, http.StatusForbidden, rec.Body.String()) + } + if rec := objectAttributesSSECRequest(t, apiRouter, credentials, bucketName, object, nil); rec.Code != http.StatusBadRequest { + t.Fatalf("%s/%s: missing key returned %d, want %d: %s", instanceType, test.name, rec.Code, http.StatusBadRequest, rec.Body.String()) + } + if rec := objectAttributesSSECRequest(t, apiRouter, credentials, bucketName, object, map[string]string{ + xhttp.MinIOSourceReplicationRequest: "true", + }); rec.Code != http.StatusOK { + t.Fatalf("%s/%s: replication request returned %d: %s", instanceType, test.name, rec.Code, rec.Body.String()) + } + } +} + +func objectAttributesSSECRequest(t *testing.T, apiRouter http.Handler, credentials auth.Credentials, + bucket, object string, encryptionHeaders map[string]string, +) *httptest.ResponseRecorder { + t.Helper() + headers := map[string]string{xhttp.AmzObjectAttributes: "ObjectSize,ETag,ObjectParts,Checksum"} + for key, value := range encryptionHeaders { + headers[key] = value + } + req, err := newTestSignedRequestV4(http.MethodGet, getGetObjectURL("", bucket, object)+"?attributes", + 0, nil, credentials.AccessKey, credentials.SecretKey, headers) + if err != nil { + t.Fatal(err) + } + rec := httptest.NewRecorder() + apiRouter.ServeHTTP(rec, req) + return rec +} diff --git a/cmd/object-checksum-unsupported_test.go b/cmd/object-checksum-unsupported_test.go new file mode 100644 index 000000000..be11f8e9c --- /dev/null +++ b/cmd/object-checksum-unsupported_test.go @@ -0,0 +1,137 @@ +// Copyright (c) 2015-2026 MinIO, Inc. +// +// This file is part of MinIO Object Storage stack +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package cmd + +import ( + "bytes" + "encoding/base64" + "encoding/xml" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/minio/minio/internal/auth" + xhttp "github.com/minio/minio/internal/http" +) + +func TestAPIRejectsUnsupportedChecksumHeaders(t *testing.T) { + defer DetectTestLeak(t)() + ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{ + t: t, + objAPITest: testAPIRejectsUnsupportedChecksumHeaders, + endpoints: []string{"CopyObject", "NewMultipart", "PutObject", "PutObjectPart"}, + }) +} + +func testAPIRejectsUnsupportedChecksumHeaders(obj ObjectLayer, instanceType, bucketName string, + apiRouter http.Handler, credentials auth.Credentials, t *testing.T, +) { + data := []byte("unsupported-checksum") + unsupportedValue := base64.StdEncoding.EncodeToString(make([]byte, 64)) + + put := func(object string, headers map[string]string) *httptest.ResponseRecorder { + t.Helper() + req, err := newTestSignedRequestV4(http.MethodPut, getPutObjectURL("", bucketName, object), + int64(len(data)), bytes.NewReader(data), credentials.AccessKey, credentials.SecretKey, headers) + if err != nil { + t.Fatal(err) + } + rec := httptest.NewRecorder() + apiRouter.ServeHTTP(rec, req) + return rec + } + assertRejected := func(name string, rec *httptest.ResponseRecorder) { + t.Helper() + if rec.Code != http.StatusBadRequest || !strings.Contains(rec.Body.String(), "InvalidArgument") { + t.Fatalf("%s: %s returned %d, want InvalidArgument: %s", instanceType, name, rec.Code, rec.Body.String()) + } + } + + for _, algorithm := range []string{"md5", "sha512", "xxhash64", "xxhash3", "xxhash128", "future"} { + object := "checksums/unsupported-" + algorithm + assertRejected(algorithm, put(object, map[string]string{ + "x-amz-sdk-checksum-algorithm": "SHA512", + "x-amz-checksum-" + algorithm: unsupportedValue, + })) + if _, err := obj.GetObjectInfo(t.Context(), bucketName, object, ObjectOptions{}); !isErrObjectNotFound(err) { + t.Fatalf("%s: rejected %s checksum stored an object: %v", instanceType, algorithm, err) + } + } + + assertRejected("unsupported trailer", put("checksums/unsupported-trailer", map[string]string{ + xhttp.AmzTrailer: "x-amz-checksum-sha512", + })) + + newMultipart := func(name string, headers map[string]string) *httptest.ResponseRecorder { + t.Helper() + req, err := newTestSignedRequestV4(http.MethodPost, getNewMultipartURL("", bucketName, name), + 0, nil, credentials.AccessKey, credentials.SecretKey, headers) + if err != nil { + t.Fatal(err) + } + rec := httptest.NewRecorder() + apiRouter.ServeHTTP(rec, req) + return rec + } + assertRejected("NewMultipartUpload value header", newMultipart("checksums/mp-value", map[string]string{ + "x-amz-checksum-sha512": unsupportedValue, + })) + assertRejected("NewMultipartUpload trailer", newMultipart("checksums/mp-trailer", map[string]string{ + xhttp.AmzTrailer: "x-amz-checksum-sha512", + })) + + rec := newMultipart("checksums/mp-part", nil) + if rec.Code != http.StatusOK { + t.Fatalf("%s: NewMultipartUpload setup returned %d: %s", instanceType, rec.Code, rec.Body.String()) + } + var initiated InitiateMultipartUploadResponse + if err := xml.Unmarshal(rec.Body.Bytes(), &initiated); err != nil { + t.Fatal(err) + } + req, err := newTestSignedRequestV4(http.MethodPut, + getPutObjectPartURL("", bucketName, "checksums/mp-part", initiated.UploadID, "1"), + int64(len(data)), bytes.NewReader(data), credentials.AccessKey, credentials.SecretKey, + map[string]string{"x-amz-checksum-sha512": unsupportedValue}) + if err != nil { + t.Fatal(err) + } + rec = httptest.NewRecorder() + apiRouter.ServeHTTP(rec, req) + assertRejected("UploadPart", rec) + parts, err := obj.ListObjectParts(t.Context(), bucketName, "checksums/mp-part", initiated.UploadID, 0, 1000, ObjectOptions{}) + if err != nil { + t.Fatal(err) + } + if len(parts.Parts) != 0 { + t.Fatalf("%s: rejected UploadPart stored %d parts", instanceType, len(parts.Parts)) + } + if err := obj.AbortMultipartUpload(t.Context(), bucketName, "checksums/mp-part", initiated.UploadID, ObjectOptions{}); err != nil { + t.Fatal(err) + } + + source := "checksums/source" + putCopyChecksumSource(t, apiRouter, credentials, bucketName, source, data, nil) + rec = copyChecksumRequest(t, apiRouter, credentials, bucketName, source, "checksums/copy", map[string]string{ + "x-amz-checksum-sha512": unsupportedValue, + }) + assertRejected("CopyObject", rec) + if _, err := obj.GetObjectInfo(t.Context(), bucketName, "checksums/copy", ObjectOptions{}); !isErrObjectNotFound(err) { + t.Fatalf("%s: rejected CopyObject stored a destination: %v", instanceType, err) + } +} diff --git a/cmd/object-copy-checksum_test.go b/cmd/object-copy-checksum_test.go new file mode 100644 index 000000000..571e781d5 --- /dev/null +++ b/cmd/object-copy-checksum_test.go @@ -0,0 +1,856 @@ +// Copyright (c) 2015-2026 MinIO, Inc. +// +// This file is part of MinIO Object Storage stack +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package cmd + +import ( + "bytes" + "crypto/md5" + "encoding/base64" + "encoding/hex" + "encoding/xml" + "io" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/minio/minio/internal/auth" + objectreplication "github.com/minio/minio/internal/bucket/replication" + "github.com/minio/minio/internal/hash" + xhttp "github.com/minio/minio/internal/http" + "github.com/minio/minio/internal/kms" +) + +func setCopyChecksumCompression(allowEncrypted bool) func() { + globalCompressConfigMu.Lock() + previous := globalCompressConfig + globalCompressConfig.Enabled = true + globalCompressConfig.Extensions = []string{".txt"} + globalCompressConfig.MimeTypes = nil + globalCompressConfig.AllowEncrypted = allowEncrypted + globalCompressConfigMu.Unlock() + + return func() { + globalCompressConfigMu.Lock() + globalCompressConfig = previous + globalCompressConfigMu.Unlock() + } +} + +func copyChecksumRequest(t *testing.T, apiRouter http.Handler, credentials auth.Credentials, + bucket, source, destination string, headers map[string]string, +) *httptest.ResponseRecorder { + t.Helper() + req, err := newTestSignedRequestV4(http.MethodPut, getCopyObjectURL("", bucket, destination), + 0, nil, credentials.AccessKey, credentials.SecretKey, headers) + if err != nil { + t.Fatalf("failed to build CopyObject request: %v", err) + } + req.Header.Set(xhttp.AmzCopySource, SlashSeparator+pathJoin(bucket, source)) + rec := httptest.NewRecorder() + apiRouter.ServeHTTP(rec, req) + return rec +} + +func putCopyChecksumSource(t *testing.T, apiRouter http.Handler, credentials auth.Credentials, + bucket, object string, data []byte, headers map[string]string, +) { + t.Helper() + req, err := newTestSignedRequestV4(http.MethodPut, getPutObjectURL("", bucket, object), + int64(len(data)), bytes.NewReader(data), credentials.AccessKey, credentials.SecretKey, headers) + if err != nil { + t.Fatalf("failed to build PutObject request: %v", err) + } + rec := httptest.NewRecorder() + apiRouter.ServeHTTP(rec, req) + if rec.Code != http.StatusOK { + t.Fatalf("PutObject(%s) failed: %d %s", object, rec.Code, rec.Body.String()) + } +} + +func readCopyChecksumObject(t *testing.T, obj ObjectLayer, bucket, object string, opts ObjectOptions) []byte { + t.Helper() + gr, err := obj.GetObjectNInfo(t.Context(), bucket, object, nil, nil, opts) + if err != nil { + t.Fatalf("GetObjectNInfo(%s) failed: %v", object, err) + } + defer gr.Close() + data, err := io.ReadAll(gr) + if err != nil { + t.Fatalf("reading %s failed: %v", object, err) + } + return data +} + +func assertCopyChecksum(t *testing.T, obj ObjectLayer, bucket, object string, typ hash.ChecksumType, + data []byte, compressed bool, decryptHeaders http.Header, +) ObjectInfo { + t.Helper() + oi, err := obj.GetObjectInfo(t.Context(), bucket, object, ObjectOptions{}) + if err != nil { + t.Fatalf("GetObjectInfo(%s) failed: %v", object, err) + } + if oi.IsCompressed() != compressed { + t.Fatalf("%s compressed=%v, want %v", object, oi.IsCompressed(), compressed) + } + checksums, _ := oi.decryptChecksums(0, decryptHeaders) + if got, want := checksums[typ.String()], mustChecksum(t, typ, data); got != want { + t.Fatalf("%s stored %s checksum %q, want logical object checksum %q (all: %v)", + object, typ.String(), got, want, checksums) + } + if got := checksums[xhttp.AmzChecksumType]; got != xhttp.AmzChecksumTypeFullObject { + t.Fatalf("%s checksum type %q, want %q", object, got, xhttp.AmzChecksumTypeFullObject) + } + return oi +} + +func assertCopyChecksumResponse(t *testing.T, rec *httptest.ResponseRecorder, typ hash.ChecksumType, data []byte) { + t.Helper() + var response CopyObjectResponse + if err := xml.Unmarshal(rec.Body.Bytes(), &response); err != nil { + t.Fatalf("unable to decode CopyObjectResult: %v", err) + } + var got string + switch typ.Base() { + case hash.ChecksumCRC32: + got = response.ChecksumCRC32 + case hash.ChecksumCRC32C: + got = response.ChecksumCRC32C + case hash.ChecksumSHA1: + got = response.ChecksumSHA1 + case hash.ChecksumSHA256: + got = response.ChecksumSHA256 + case hash.ChecksumCRC64NVME: + got = response.ChecksumCRC64NVME + } + if want := mustChecksum(t, typ, data); got != want { + t.Fatalf("CopyObjectResult %s checksum %q, want %q: %s", typ.String(), got, want, rec.Body.String()) + } + if response.ChecksumType != xhttp.AmzChecksumTypeFullObject { + t.Fatalf("CopyObjectResult checksum type %q, want %q", response.ChecksumType, xhttp.AmzChecksumTypeFullObject) + } +} + +// TestAPICopyObjectServerSideChecksum verifies that server-computed checksums +// cover the logical object, never the compressed storage stream. +func TestAPICopyObjectServerSideChecksum(t *testing.T) { + defer DetectTestLeak(t)() + for _, versioned := range []bool{false, true} { + name := "unversioned" + if versioned { + name = "versioned" + } + t.Run(name, func(t *testing.T) { + ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{ + t: t, + objAPITest: testAPICopyObjectServerSideChecksum, + endpoints: []string{"CopyObject", "PutObject", "HeadObject", "GetObject"}, + makeBucketOptions: MakeBucketOptions{VersioningEnabled: versioned}, + }) + }) + } +} + +func testAPICopyObjectServerSideChecksum(obj ObjectLayer, instanceType, bucketName string, + apiRouter http.Handler, credentials auth.Credentials, t *testing.T, +) { + restoreCompression := setCopyChecksumCompression(true) + defer restoreCompression() + + data := bytes.Repeat([]byte("copy-object-checksum-plaintext-"), 64*1024) + source := "copy-checksum/source.bin" + if _, err := obj.PutObject(t.Context(), bucketName, source, + mustGetPutObjReader(t, bytes.NewReader(data), int64(len(data)), "", ""), ObjectOptions{}); err != nil { + t.Fatalf("%s: source PutObject failed: %v", instanceType, err) + } + + compressedReader, _ := newS2CompressReader(bytes.NewReader(data), int64(len(data)), false) + compressed, err := io.ReadAll(compressedReader) + if closeErr := compressedReader.Close(); err == nil { + err = closeErr + } + if err != nil { + t.Fatalf("%s: independently compressing test data failed: %v", instanceType, err) + } + + cases := []struct { + name string + typ hash.ChecksumType + explicit bool + extension string + compressed bool + }{ + {name: "compressed/CRC32", typ: hash.ChecksumCRC32, explicit: true, extension: ".txt", compressed: true}, + {name: "compressed/CRC32C", typ: hash.ChecksumCRC32C, explicit: true, extension: ".txt", compressed: true}, + {name: "compressed/SHA1", typ: hash.ChecksumSHA1, explicit: true, extension: ".txt", compressed: true}, + {name: "compressed/SHA256", typ: hash.ChecksumSHA256, explicit: true, extension: ".txt", compressed: true}, + {name: "compressed/CRC64NVME", typ: hash.ChecksumCRC64NVME, explicit: true, extension: ".txt", compressed: true}, + {name: "compressed/default", typ: hash.ChecksumCRC64NVME, extension: ".txt", compressed: true}, + {name: "plain/CRC32", typ: hash.ChecksumCRC32, explicit: true, extension: ".bin"}, + {name: "plain/default", typ: hash.ChecksumCRC64NVME, extension: ".bin"}, + } + + for i, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + headers := map[string]string(nil) + if tc.explicit { + headers = map[string]string{xhttp.AmzChecksumAlgo: tc.typ.String()} + } + destination := "copy-checksum/destination-" + tc.typ.String() + "-" + string(rune('a'+i)) + tc.extension + rec := copyChecksumRequest(t, apiRouter, credentials, bucketName, source, destination, headers) + if rec.Code != http.StatusOK { + t.Fatalf("%s: CopyObject failed: %d %s", instanceType, rec.Code, rec.Body.String()) + } + assertCopyChecksumResponse(t, rec, tc.typ, data) + + info := assertCopyChecksum(t, obj, bucketName, destination, tc.typ, data, tc.compressed, nil) + md5sum := md5.Sum(data) + if got, want := info.ETag, hex.EncodeToString(md5sum[:]); got != want { + t.Fatalf("%s: ETag %q, want logical object MD5 %q", instanceType, got, want) + } + if tc.compressed { + logical := mustChecksum(t, tc.typ, data) + if transformed := mustChecksum(t, tc.typ, compressed); logical == transformed { + t.Fatalf("%s: test payload does not distinguish logical and compressed checksum domains", instanceType) + } + } + if got := readCopyChecksumObject(t, obj, bucketName, destination, ObjectOptions{}); !bytes.Equal(got, data) { + t.Fatalf("%s: round-trip body differs for %s", instanceType, tc.name) + } + + if tc.name == "compressed/CRC32" { + for _, method := range []string{http.MethodHead, http.MethodGet} { + url := getHeadObjectURL("", bucketName, destination) + if method == http.MethodGet { + url = getGetObjectURL("", bucketName, destination) + } + req, err := newTestSignedRequestV4(method, url, 0, nil, + credentials.AccessKey, credentials.SecretKey, + map[string]string{xhttp.AmzChecksumMode: "ENABLED"}) + if err != nil { + t.Fatalf("failed to build %s request: %v", method, err) + } + response := httptest.NewRecorder() + apiRouter.ServeHTTP(response, req) + if response.Code != http.StatusOK { + t.Fatalf("%s returned %d: %s", method, response.Code, response.Body.String()) + } + if got, want := response.Header().Get(tc.typ.Key()), mustChecksum(t, tc.typ, data); got != want { + t.Fatalf("%s returned checksum %q, want %q", method, got, want) + } + if method == http.MethodGet && !bytes.Equal(response.Body.Bytes(), data) { + t.Fatalf("GET response body differs") + } + } + } + }) + } +} + +func TestAPICopyObjectServerSideChecksumEncryption(t *testing.T) { + defer DetectTestLeak(t)() + ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{ + t: t, + objAPITest: testAPICopyObjectServerSideChecksumEncryption, + endpoints: []string{"CopyObject", "PutObject", "GetObject"}, + }) +} + +func testAPICopyObjectServerSideChecksumEncryption(obj ObjectLayer, instanceType, bucketName string, + apiRouter http.Handler, credentials auth.Credentials, t *testing.T, +) { + restoreCompression := setCopyChecksumCompression(true) + defer restoreCompression() + + data := bytes.Repeat([]byte("encrypted-copy-checksum-plaintext-"), 48*1024) + source := "copy-checksum/encrypted-source.bin" + if _, err := obj.PutObject(t.Context(), bucketName, source, + mustGetPutObjReader(t, bytes.NewReader(data), int64(len(data)), "", ""), ObjectOptions{}); err != nil { + t.Fatalf("%s: source PutObject failed: %v", instanceType, err) + } + + t.Run("SSE-S3", func(t *testing.T) { + secretKey, err := kms.ParseSecretKey("my-minio-key:5lF+0pJM0OWwlQrvK2S/I7W9mO4a6rJJI7wzj7v09cw=") + if err != nil { + t.Fatal(err) + } + previousKMS := GlobalKMS + GlobalKMS = secretKey + defer func() { GlobalKMS = previousKMS }() + + for _, variant := range []struct { + name string + extension string + compressed bool + }{ + {name: "encrypted-only", extension: ".bin"}, + {name: "compressed-encrypted", extension: ".txt", compressed: true}, + } { + t.Run(variant.name, func(t *testing.T) { + destination := "copy-checksum/sse-s3-" + variant.name + variant.extension + rec := copyChecksumRequest(t, apiRouter, credentials, bucketName, source, destination, map[string]string{ + xhttp.AmzChecksumAlgo: hash.ChecksumCRC32.String(), + xhttp.AmzServerSideEncryption: xhttp.AmzEncryptionAES, + }) + if rec.Code != http.StatusOK { + t.Fatalf("%s: SSE-S3 CopyObject failed: %d %s", instanceType, rec.Code, rec.Body.String()) + } + assertCopyChecksumResponse(t, rec, hash.ChecksumCRC32, data) + assertCopyChecksum(t, obj, bucketName, destination, hash.ChecksumCRC32, data, variant.compressed, nil) + if got := readCopyChecksumObject(t, obj, bucketName, destination, ObjectOptions{}); !bytes.Equal(got, data) { + t.Fatalf("%s: SSE-S3 round-trip body differs", instanceType) + } + }) + } + + encryptedSource := "copy-checksum/sse-s3-source.bin" + putCopyChecksumSource(t, apiRouter, credentials, bucketName, encryptedSource, data, + map[string]string{xhttp.AmzServerSideEncryption: xhttp.AmzEncryptionAES}) + destination := "copy-checksum/sse-s3-source-copy.txt" + rec := copyChecksumRequest(t, apiRouter, credentials, bucketName, encryptedSource, destination, + map[string]string{xhttp.AmzChecksumAlgo: hash.ChecksumCRC32.String()}) + if rec.Code != http.StatusOK { + t.Fatalf("%s: SSE-S3 source CopyObject failed: %d %s", instanceType, rec.Code, rec.Body.String()) + } + assertCopyChecksumResponse(t, rec, hash.ChecksumCRC32, data) + assertCopyChecksum(t, obj, bucketName, destination, hash.ChecksumCRC32, data, true, nil) + if got := readCopyChecksumObject(t, obj, bucketName, destination, ObjectOptions{}); !bytes.Equal(got, data) { + t.Fatalf("%s: SSE-S3 source round-trip body differs", instanceType) + } + }) + + t.Run("SSE-C", func(t *testing.T) { + previousTLS := globalIsTLS + globalIsTLS = true + defer func() { globalIsTLS = previousTLS }() + + key := bytes.Repeat([]byte{0x2a}, 32) + keyMD5 := md5.Sum(key) + headers := map[string]string{ + xhttp.AmzChecksumAlgo: hash.ChecksumCRC32.String(), + xhttp.AmzServerSideEncryptionCustomerAlgorithm: xhttp.AmzEncryptionAES, + xhttp.AmzServerSideEncryptionCustomerKey: base64.StdEncoding.EncodeToString(key), + xhttp.AmzServerSideEncryptionCustomerKeyMD5: base64.StdEncoding.EncodeToString(keyMD5[:]), + } + decryptHeaders := http.Header{} + for key, value := range headers { + decryptHeaders.Set(key, value) + } + + getHeaders := make(map[string]string, len(headers)) + for key, value := range headers { + if key != xhttp.AmzChecksumAlgo { + getHeaders[key] = value + } + } + for _, variant := range []struct { + name string + extension string + compressed bool + }{ + {name: "encrypted-only", extension: ".bin"}, + // SSE-C is excluded from compression whatever allow_encryption says, + // so a compressible destination extension changes nothing here. The + // SSE-S3 sibling above keeps the compressed-encrypted coverage. + {name: "compressible-extension", extension: ".txt"}, + } { + t.Run(variant.name, func(t *testing.T) { + destination := "copy-checksum/sse-c-" + variant.name + variant.extension + rec := copyChecksumRequest(t, apiRouter, credentials, bucketName, source, destination, headers) + if rec.Code != http.StatusOK { + t.Fatalf("%s: SSE-C CopyObject failed: %d %s", instanceType, rec.Code, rec.Body.String()) + } + assertCopyChecksumResponse(t, rec, hash.ChecksumCRC32, data) + assertCopyChecksum(t, obj, bucketName, destination, hash.ChecksumCRC32, data, variant.compressed, decryptHeaders) + + req, err := newTestSignedRequestV4(http.MethodGet, getGetObjectURL("", bucketName, destination), + 0, nil, credentials.AccessKey, credentials.SecretKey, getHeaders) + if err != nil { + t.Fatalf("failed to build SSE-C GetObject request: %v", err) + } + response := httptest.NewRecorder() + apiRouter.ServeHTTP(response, req) + if response.Code != http.StatusOK || !bytes.Equal(response.Body.Bytes(), data) { + t.Fatalf("%s: SSE-C GetObject returned %d with %d bytes, want 200 with %d bytes", + instanceType, response.Code, response.Body.Len(), len(data)) + } + }) + } + + oldKey := bytes.Repeat([]byte{0x31}, 32) + oldKeyMD5 := md5.Sum(oldKey) + newKey := bytes.Repeat([]byte{0x42}, 32) + newKeyMD5 := md5.Sum(newKey) + encryptedSource := "copy-checksum/sse-c-different-key-source.bin" + putCopyChecksumSource(t, apiRouter, credentials, bucketName, encryptedSource, data, map[string]string{ + xhttp.AmzServerSideEncryptionCustomerAlgorithm: xhttp.AmzEncryptionAES, + xhttp.AmzServerSideEncryptionCustomerKey: base64.StdEncoding.EncodeToString(oldKey), + xhttp.AmzServerSideEncryptionCustomerKeyMD5: base64.StdEncoding.EncodeToString(oldKeyMD5[:]), + }) + + destination := "copy-checksum/sse-c-different-key-destination.bin" + rec := copyChecksumRequest(t, apiRouter, credentials, bucketName, encryptedSource, destination, map[string]string{ + xhttp.AmzChecksumAlgo: hash.ChecksumCRC32.String(), + xhttp.AmzServerSideEncryptionCustomerAlgorithm: xhttp.AmzEncryptionAES, + xhttp.AmzServerSideEncryptionCustomerKey: base64.StdEncoding.EncodeToString(newKey), + xhttp.AmzServerSideEncryptionCustomerKeyMD5: base64.StdEncoding.EncodeToString(newKeyMD5[:]), + xhttp.AmzServerSideEncryptionCopyCustomerAlgorithm: xhttp.AmzEncryptionAES, + xhttp.AmzServerSideEncryptionCopyCustomerKey: base64.StdEncoding.EncodeToString(oldKey), + xhttp.AmzServerSideEncryptionCopyCustomerKeyMD5: base64.StdEncoding.EncodeToString(oldKeyMD5[:]), + }) + if rec.Code != http.StatusOK { + t.Fatalf("%s: different-key SSE-C CopyObject failed: %d %s", instanceType, rec.Code, rec.Body.String()) + } + assertCopyChecksumResponse(t, rec, hash.ChecksumCRC32, data) + if got, want := rec.Header().Get(hash.ChecksumCRC32.Key()), mustChecksum(t, hash.ChecksumCRC32, data); got != want { + t.Fatalf("%s: different-key SSE-C response header checksum %q, want %q", instanceType, got, want) + } + if got := rec.Header().Get(xhttp.AmzChecksumType); got != xhttp.AmzChecksumTypeFullObject { + t.Fatalf("%s: different-key SSE-C response checksum type %q, want %q", instanceType, got, xhttp.AmzChecksumTypeFullObject) + } + newKeyHeaders := http.Header{ + xhttp.AmzServerSideEncryptionCustomerAlgorithm: []string{xhttp.AmzEncryptionAES}, + xhttp.AmzServerSideEncryptionCustomerKey: []string{base64.StdEncoding.EncodeToString(newKey)}, + xhttp.AmzServerSideEncryptionCustomerKeyMD5: []string{base64.StdEncoding.EncodeToString(newKeyMD5[:])}, + } + assertCopyChecksum(t, obj, bucketName, destination, hash.ChecksumCRC32, data, false, newKeyHeaders) + }) +} + +func TestAPICopyObjectServerSideChecksumSourceVariants(t *testing.T) { + defer DetectTestLeak(t)() + ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{ + t: t, + objAPITest: testAPICopyObjectServerSideChecksumSourceVariants, + endpoints: []string{ + "NewMultipart", "PutObjectPart", "CompleteMultipart", "ListObjectParts", + "CopyObject", "PutObject", "HeadObject", "GetObject", + }, + }) +} + +func testAPICopyObjectServerSideChecksumSourceVariants(obj ObjectLayer, instanceType, bucketName string, + apiRouter http.Handler, credentials auth.Credentials, t *testing.T, +) { + restoreCompression := setCopyChecksumCompression(true) + defer restoreCompression() + + data := bytes.Repeat([]byte("source-variant-plaintext-"), 64*1024) + + t.Run("compressed-source", func(t *testing.T) { + source := "copy-checksum/compressed-source.txt" + putCopyChecksumSource(t, apiRouter, credentials, bucketName, source, data, nil) + if info, err := obj.GetObjectInfo(t.Context(), bucketName, source, ObjectOptions{}); err != nil || !info.IsCompressed() { + t.Fatalf("%s: compressed source precondition failed: compressed=%v err=%v", instanceType, info.IsCompressed(), err) + } + destination := "copy-checksum/compressed-source-copy.txt" + rec := copyChecksumRequest(t, apiRouter, credentials, bucketName, source, destination, + map[string]string{xhttp.AmzChecksumAlgo: hash.ChecksumCRC32.String()}) + if rec.Code != http.StatusOK { + t.Fatalf("%s: CopyObject failed: %d %s", instanceType, rec.Code, rec.Body.String()) + } + assertCopyChecksumResponse(t, rec, hash.ChecksumCRC32, data) + assertCopyChecksum(t, obj, bucketName, destination, hash.ChecksumCRC32, data, true, nil) + + rec = copyChecksumRequest(t, apiRouter, credentials, bucketName, source, source, map[string]string{ + xhttp.AmzChecksumAlgo: hash.ChecksumSHA256.String(), + xhttp.AmzMetadataDirective: "REPLACE", + }) + if rec.Code != http.StatusOK { + t.Fatalf("%s: in-place CopyObject failed: %d %s", instanceType, rec.Code, rec.Body.String()) + } + assertCopyChecksumResponse(t, rec, hash.ChecksumSHA256, data) + assertCopyChecksum(t, obj, bucketName, source, hash.ChecksumSHA256, data, true, nil) + if got := readCopyChecksumObject(t, obj, bucketName, source, ObjectOptions{}); !bytes.Equal(got, data) { + t.Fatalf("%s: in-place CopyObject body differs", instanceType) + } + }) + + t.Run("full-checksum-source", func(t *testing.T) { + source := "copy-checksum/full-checksum-source.bin" + want := mustChecksum(t, hash.ChecksumCRC32, data) + putCopyChecksumSource(t, apiRouter, credentials, bucketName, source, data, + map[string]string{xhttp.AmzChecksumCRC32: want}) + destination := "copy-checksum/full-checksum-copy.txt" + rec := copyChecksumRequest(t, apiRouter, credentials, bucketName, source, destination, nil) + if rec.Code != http.StatusOK { + t.Fatalf("%s: CopyObject failed: %d %s", instanceType, rec.Code, rec.Body.String()) + } + assertCopyChecksumResponse(t, rec, hash.ChecksumCRC32, data) + assertCopyChecksum(t, obj, bucketName, destination, hash.ChecksumCRC32, data, true, nil) + }) + + t.Run("multipart-composite-source", func(t *testing.T) { + typ := hash.ChecksumCRC32 + parts, full := multipartChecksumTestData() + source := "copy-checksum/multipart-source.bin" + uploadID := newMultipartUploadHTTP(t, apiRouter, credentials, bucketName, source, + typ.String(), xhttp.AmzChecksumTypeComposite) + etags := uploadPartsHTTP(t, apiRouter, credentials, bucketName, source, uploadID, typ, parts) + partChecksums := make([]string, len(parts)) + for i, part := range parts { + partChecksums[i] = mustChecksum(t, typ, part) + } + rec := completeMultipartUploadHTTP(t, apiRouter, credentials, bucketName, source, uploadID, + etags, partChecksums, nil) + if rec.Code != http.StatusOK { + t.Fatalf("%s: CompleteMultipartUpload failed: %d %s", instanceType, rec.Code, rec.Body.String()) + } + sourceInfo, err := obj.GetObjectInfo(t.Context(), bucketName, source, ObjectOptions{}) + if err != nil { + t.Fatalf("%s: source GetObjectInfo failed: %v", instanceType, err) + } + if _, multipart := sourceInfo.decryptChecksums(0, nil); !multipart { + t.Fatalf("%s: source checksum is not multipart composite", instanceType) + } + + destination := "copy-checksum/multipart-copy.txt" + rec = copyChecksumRequest(t, apiRouter, credentials, bucketName, source, destination, nil) + if rec.Code != http.StatusOK { + t.Fatalf("%s: CopyObject failed: %d %s", instanceType, rec.Code, rec.Body.String()) + } + assertCopyChecksumResponse(t, rec, typ, full) + assertCopyChecksum(t, obj, bucketName, destination, typ, full, true, nil) + if got := readCopyChecksumObject(t, obj, bucketName, destination, ObjectOptions{}); !bytes.Equal(got, full) { + t.Fatalf("%s: multipart source round-trip body differs", instanceType) + } + }) + + for _, boundary := range []struct { + name string + data []byte + compressed bool + }{ + {name: "at-threshold", data: bytes.Repeat([]byte{'a'}, minCompressibleSize)}, + {name: "over-threshold", data: bytes.Repeat([]byte{'a'}, minCompressibleSize+1), compressed: true}, + {name: "indexed", data: bytes.Repeat([]byte{'a'}, compMinIndexSize+1), compressed: true}, + {name: "empty", data: nil}, + } { + t.Run(boundary.name, func(t *testing.T) { + source := "copy-checksum/" + boundary.name + "-source.bin" + if _, err := obj.PutObject(t.Context(), bucketName, source, + mustGetPutObjReader(t, bytes.NewReader(boundary.data), int64(len(boundary.data)), "", ""), ObjectOptions{}); err != nil { + t.Fatalf("%s: source PutObject failed: %v", instanceType, err) + } + destination := "copy-checksum/" + boundary.name + "-copy.txt" + rec := copyChecksumRequest(t, apiRouter, credentials, bucketName, source, destination, + map[string]string{xhttp.AmzChecksumAlgo: hash.ChecksumCRC32.String()}) + if rec.Code != http.StatusOK { + t.Fatalf("%s: CopyObject failed: %d %s", instanceType, rec.Code, rec.Body.String()) + } + assertCopyChecksumResponse(t, rec, hash.ChecksumCRC32, boundary.data) + assertCopyChecksum(t, obj, bucketName, destination, hash.ChecksumCRC32, boundary.data, boundary.compressed, nil) + }) + } +} + +func TestPutObjectRejectsMissingServerSideChecksum(t *testing.T) { + defer DetectTestLeak(t)() + ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{ + t: t, + objAPITest: testPutObjectRejectsMissingServerSideChecksum, + endpoints: []string{"PutObject"}, + }) +} + +func testPutObjectRejectsMissingServerSideChecksum(obj ObjectLayer, instanceType, bucketName string, + _ http.Handler, _ auth.Credentials, t *testing.T, +) { + data := []byte("the object layer must not silently omit a requested checksum") + for _, test := range []struct { + name string + hasherType hash.ChecksumType + }{ + {name: "missing"}, + {name: "mismatched", hasherType: hash.ChecksumCRC32C}, + } { + t.Run(test.name, func(t *testing.T) { + object := "copy-checksum/" + test.name + "-server-side-checksum" + reader := mustGetPutObjReader(t, bytes.NewReader(data), int64(len(data)), "", "") + if test.hasherType.IsSet() { + reader.AddServerSideChecksumHasher(test.hasherType) + } + _, err := obj.PutObject(t.Context(), bucketName, object, reader, + ObjectOptions{WantServerSideChecksumType: hash.ChecksumCRC32}) + if err == nil || !strings.Contains(err.Error(), "server-side checksum") { + t.Fatalf("%s: PutObject error %v, want server-side checksum invariant error", instanceType, err) + } + if _, err = obj.GetObjectInfo(t.Context(), bucketName, object, ObjectOptions{}); !isErrObjectNotFound(err) { + t.Fatalf("%s: failed PutObject left an object behind: %v", instanceType, err) + } + }) + } +} + +// copyChecksumSSECHeaders returns the SSE-C headers naming key for a request +// that reads or writes the object itself. +func copyChecksumSSECHeaders(key []byte) map[string]string { + digest := md5.Sum(key) + return map[string]string{ + xhttp.AmzServerSideEncryptionCustomerAlgorithm: xhttp.AmzEncryptionAES, + xhttp.AmzServerSideEncryptionCustomerKey: base64.StdEncoding.EncodeToString(key), + xhttp.AmzServerSideEncryptionCustomerKeyMD5: base64.StdEncoding.EncodeToString(digest[:]), + } +} + +// copyChecksumSSECCopySource returns the SSE-C headers naming key as the +// CopyObject source key. +func copyChecksumSSECCopySource(key []byte) map[string]string { + digest := md5.Sum(key) + return map[string]string{ + xhttp.AmzServerSideEncryptionCopyCustomerAlgorithm: xhttp.AmzEncryptionAES, + xhttp.AmzServerSideEncryptionCopyCustomerKey: base64.StdEncoding.EncodeToString(key), + xhttp.AmzServerSideEncryptionCopyCustomerKeyMD5: base64.StdEncoding.EncodeToString(digest[:]), + } +} + +// TestAPICopyObjectSSECKeyRotationChecksumAlgorithm covers an in-place SSE-C key +// rotation that also requests a different checksum algorithm. The rotation fast +// path only rewraps the object key and never reads the object data, so it cannot +// honor the request; the copy has to fall through to the re-encrypting path that +// recomputes, stores and reports the requested algorithm. +func TestAPICopyObjectSSECKeyRotationChecksumAlgorithm(t *testing.T) { + defer DetectTestLeak(t)() + ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{ + t: t, + objAPITest: testAPICopyObjectSSECKeyRotationChecksumAlgorithm, + endpoints: []string{"CopyObject", "PutObject", "GetObject"}, + }) +} + +func testAPICopyObjectSSECKeyRotationChecksumAlgorithm(obj ObjectLayer, instanceType, bucketName string, + apiRouter http.Handler, credentials auth.Credentials, t *testing.T, +) { + previousTLS := globalIsTLS + globalIsTLS = true + defer func() { globalIsTLS = previousTLS }() + + data := bytes.Repeat([]byte("rotate-and-upgrade-the-checksum-"), 32*1024) + object := "copy-checksum/rotate-checksum-algorithm.bin" + oldKey := bytes.Repeat([]byte{0x31}, 32) + newKey := bytes.Repeat([]byte{0x42}, 32) + + putHeaders := copyChecksumSSECHeaders(oldKey) + putHeaders[xhttp.AmzChecksumCRC32] = mustChecksum(t, hash.ChecksumCRC32, data) + putCopyChecksumSource(t, apiRouter, credentials, bucketName, object, data, putHeaders) + + rotate := copyChecksumSSECHeaders(newKey) + rotate[xhttp.AmzChecksumAlgo] = hash.ChecksumSHA256.String() + for name, value := range copyChecksumSSECCopySource(oldKey) { + rotate[name] = value + } + rec := copyChecksumRequest(t, apiRouter, credentials, bucketName, object, object, rotate) + if rec.Code != http.StatusOK { + t.Fatalf("%s: rotation with a requested algorithm failed: %d %s", + instanceType, rec.Code, rec.Body.String()) + } + assertCopyChecksumResponse(t, rec, hash.ChecksumSHA256, data) + + decryptHeaders := http.Header{} + for name, value := range copyChecksumSSECHeaders(newKey) { + decryptHeaders.Set(name, value) + } + oi := assertCopyChecksum(t, obj, bucketName, object, hash.ChecksumSHA256, data, false, decryptHeaders) + if stored, _ := oi.decryptChecksums(0, decryptHeaders); stored[hash.ChecksumCRC32.String()] != "" { + t.Fatalf("%s: rotation kept the superseded CRC32 checksum: %v", instanceType, stored) + } + + getHeaders := copyChecksumSSECHeaders(newKey) + getHeaders[xhttp.AmzChecksumMode] = "ENABLED" + req, err := newTestSignedRequestV4(http.MethodGet, getGetObjectURL("", bucketName, object), + 0, nil, credentials.AccessKey, credentials.SecretKey, getHeaders) + if err != nil { + t.Fatalf("failed to build GetObject request: %v", err) + } + response := httptest.NewRecorder() + apiRouter.ServeHTTP(response, req) + if response.Code != http.StatusOK || !bytes.Equal(response.Body.Bytes(), data) { + t.Fatalf("%s: post-rotation GetObject returned %d with %d bytes, want 200 with %d bytes: %s", + instanceType, response.Code, response.Body.Len(), len(data), response.Body.String()) + } + if got, want := response.Header().Get(xhttp.AmzChecksumSHA256), + mustChecksum(t, hash.ChecksumSHA256, data); got != want { + t.Fatalf("%s: post-rotation GetObject SHA256 %q, want %q", instanceType, got, want) + } + if got := response.Header().Get(xhttp.AmzChecksumCRC32); got != "" { + t.Fatalf("%s: post-rotation GetObject still returns the superseded CRC32 %q", instanceType, got) + } + + stale, err := newTestSignedRequestV4(http.MethodGet, getGetObjectURL("", bucketName, object), + 0, nil, credentials.AccessKey, credentials.SecretKey, copyChecksumSSECHeaders(oldKey)) + if err != nil { + t.Fatalf("failed to build stale-key GetObject request: %v", err) + } + staleResponse := httptest.NewRecorder() + apiRouter.ServeHTTP(staleResponse, stale) + if staleResponse.Code != http.StatusForbidden { + t.Fatalf("%s: GetObject with the rotated-out key returned %d, want 403", + instanceType, staleResponse.Code) + } +} + +// TestAPICopyObjectSSECKeyRotationKeepsChecksumAbsence pins the compatibility +// limitation accepted with pgsty/silo#113: without a requested algorithm an +// in-place SSE-C rotation preserves the stored checksum state, including its +// absence, so a checksum-less object does not gain the CRC64NVME that every +// re-encrypting CopyObject adds. Deliberate, and the counterpart of the +// preserved CRC32 that TestAPICopyObjectSSECKeyRotationKeepsCompressionState +// pins for a checksum-bearing source. +func TestAPICopyObjectSSECKeyRotationKeepsChecksumAbsence(t *testing.T) { + defer DetectTestLeak(t)() + ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{ + t: t, + objAPITest: testAPICopyObjectSSECKeyRotationKeepsChecksumAbsence, + endpoints: []string{"CopyObject", "PutObject", "GetObject"}, + }) +} + +func testAPICopyObjectSSECKeyRotationKeepsChecksumAbsence(obj ObjectLayer, instanceType, bucketName string, + apiRouter http.Handler, credentials auth.Credentials, t *testing.T, +) { + previousTLS := globalIsTLS + globalIsTLS = true + defer func() { globalIsTLS = previousTLS }() + + data := bytes.Repeat([]byte("rotate-without-a-checksum-"), 32*1024) + object := "copy-checksum/rotate-keeps-checksum-absence.bin" + oldKey := bytes.Repeat([]byte{0x53}, 32) + newKey := bytes.Repeat([]byte{0x64}, 32) + + // Assert the raw stored bytes rather than decryptChecksums, which also + // returns an empty map when it cannot unseal a checksum that is there. + storedChecksum := func() []byte { + t.Helper() + oi, err := obj.GetObjectInfo(t.Context(), bucketName, object, ObjectOptions{}) + if err != nil { + t.Fatalf("GetObjectInfo(%s) failed: %v", object, err) + } + return oi.Checksum + } + + putCopyChecksumSource(t, apiRouter, credentials, bucketName, object, data, copyChecksumSSECHeaders(oldKey)) + if before := storedChecksum(); len(before) != 0 { + t.Fatalf("%s: invalid precondition, the source already carries a checksum: %x", instanceType, before) + } + + rotate := copyChecksumSSECHeaders(newKey) + for name, value := range copyChecksumSSECCopySource(oldKey) { + rotate[name] = value + } + rec := copyChecksumRequest(t, apiRouter, credentials, bucketName, object, object, rotate) + if rec.Code != http.StatusOK { + t.Fatalf("%s: headerless rotation failed: %d %s", instanceType, rec.Code, rec.Body.String()) + } + var copyResponse CopyObjectResponse + if err := xml.Unmarshal(rec.Body.Bytes(), ©Response); err != nil { + t.Fatalf("unable to decode CopyObjectResult: %v", err) + } + if copyResponse.ChecksumCRC32 != "" || copyResponse.ChecksumCRC32C != "" || + copyResponse.ChecksumSHA1 != "" || copyResponse.ChecksumSHA256 != "" || + copyResponse.ChecksumCRC64NVME != "" || copyResponse.ChecksumType != "" { + t.Fatalf("%s: headerless rotation reported a checksum it did not compute: %s", + instanceType, rec.Body.String()) + } + if after := storedChecksum(); len(after) != 0 { + t.Fatalf("%s: headerless rotation attached a checksum: %x", instanceType, after) + } + + req, err := newTestSignedRequestV4(http.MethodGet, getGetObjectURL("", bucketName, object), + 0, nil, credentials.AccessKey, credentials.SecretKey, copyChecksumSSECHeaders(newKey)) + if err != nil { + t.Fatalf("failed to build GetObject request: %v", err) + } + response := httptest.NewRecorder() + apiRouter.ServeHTTP(response, req) + if response.Code != http.StatusOK || !bytes.Equal(response.Body.Bytes(), data) { + t.Fatalf("%s: post-rotation GetObject returned %d with %d bytes, want 200 with %d bytes: %s", + instanceType, response.Code, response.Body.Len(), len(data), response.Body.String()) + } +} + +// TestAPICopyObjectSSECKeyRotationReplicaKeepsFastPath pins that a replica-trusted +// rotation keeps the in-place fast path even when it carries a checksum algorithm +// header. Such a request reads its source without decrypting it, so a rewrite +// would hash ciphertext and would skip the source-key check that a zero byte read +// performs, and a replica has to keep the checksum its source assigned anyway. +func TestAPICopyObjectSSECKeyRotationReplicaKeepsFastPath(t *testing.T) { + defer DetectTestLeak(t)() + ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{ + t: t, + objAPITest: testAPICopyObjectSSECKeyRotationReplicaKeepsFastPath, + endpoints: []string{"CopyObject", "PutObject", "GetObject"}, + }) +} + +func testAPICopyObjectSSECKeyRotationReplicaKeepsFastPath(obj ObjectLayer, instanceType, bucketName string, + apiRouter http.Handler, credentials auth.Credentials, t *testing.T, +) { + previousTLS := globalIsTLS + globalIsTLS = true + defer func() { globalIsTLS = previousTLS }() + + oldKey := bytes.Repeat([]byte{0x71}, 32) + newKey := bytes.Repeat([]byte{0x82}, 32) + wrongKey := bytes.Repeat([]byte{0x93}, 32) + + rotateAsReplica := func(sourceKey []byte) map[string]string { + headers := copyChecksumSSECHeaders(newKey) + headers[xhttp.AmzChecksumAlgo] = hash.ChecksumSHA256.String() + for name, value := range copyChecksumSSECCopySource(sourceKey) { + headers[name] = value + } + headers[xhttp.MinIOSourceReplicationRequest] = "true" + headers[xhttp.AmzBucketReplicationStatus] = objectreplication.Replica.String() + return headers + } + + data := bytes.Repeat([]byte("replica-rotation-keeps-its-checksum-"), 1024) + object := "copy-checksum/replica-rotate.bin" + putHeaders := copyChecksumSSECHeaders(oldKey) + putHeaders[xhttp.AmzChecksumCRC32] = mustChecksum(t, hash.ChecksumCRC32, data) + putCopyChecksumSource(t, apiRouter, credentials, bucketName, object, data, putHeaders) + + rec := copyChecksumRequest(t, apiRouter, credentials, bucketName, object, object, rotateAsReplica(oldKey)) + if rec.Code != http.StatusOK { + t.Fatalf("%s: replica rotation with an algorithm header failed: %d %s", + instanceType, rec.Code, rec.Body.String()) + } + assertCopyChecksumResponse(t, rec, hash.ChecksumCRC32, data) + + req, err := newTestSignedRequestV4(http.MethodGet, getGetObjectURL("", bucketName, object), + 0, nil, credentials.AccessKey, credentials.SecretKey, copyChecksumSSECHeaders(newKey)) + if err != nil { + t.Fatalf("failed to build GetObject request: %v", err) + } + response := httptest.NewRecorder() + apiRouter.ServeHTTP(response, req) + if response.Code != http.StatusOK || !bytes.Equal(response.Body.Bytes(), data) { + t.Fatalf("%s: post-rotation GetObject returned %d with %d bytes, want 200 with %d bytes: %s", + instanceType, response.Code, response.Body.Len(), len(data), response.Body.String()) + } + + // A zero byte source is the case where the fast path is the only thing that + // still authenticates the rotated-out key. + empty := "copy-checksum/replica-rotate-empty.bin" + putCopyChecksumSource(t, apiRouter, credentials, bucketName, empty, nil, copyChecksumSSECHeaders(oldKey)) + wrong := copyChecksumRequest(t, apiRouter, credentials, bucketName, empty, empty, rotateAsReplica(wrongKey)) + if wrong.Code != http.StatusForbidden { + t.Fatalf("%s: replica rotation of an empty object with the wrong source key returned %d, want 403: %s", + instanceType, wrong.Code, wrong.Body.String()) + } +} diff --git a/cmd/object-copy-metadata_test.go b/cmd/object-copy-metadata_test.go new file mode 100644 index 000000000..2cb05f3eb --- /dev/null +++ b/cmd/object-copy-metadata_test.go @@ -0,0 +1,613 @@ +// Copyright (c) 2015-2026 MinIO, Inc. +// +// This file is part of MinIO Object Storage stack +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package cmd + +import ( + "bytes" + "crypto/md5" + "encoding/base64" + "net/http" + "net/http/httptest" + "testing" + + "github.com/minio/minio/internal/auth" + "github.com/minio/minio/internal/hash" + xhttp "github.com/minio/minio/internal/http" +) + +func TestAPICopyObjectMetadataOnlyCompression(t *testing.T) { + defer DetectTestLeak(t)() + for _, versioned := range []bool{false, true} { + name := "unversioned" + if versioned { + name = "versioned" + } + t.Run(name, func(t *testing.T) { + ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{ + t: t, + objAPITest: testAPICopyObjectMetadataOnlyCompression, + endpoints: []string{"CopyObject", "PutObject", "GetObject"}, + makeBucketOptions: MakeBucketOptions{VersioningEnabled: versioned}, + }) + }) + } +} + +func testAPICopyObjectMetadataOnlyCompression(obj ObjectLayer, instanceType, bucketName string, + apiRouter http.Handler, credentials auth.Credentials, t *testing.T, +) { + data := bytes.Repeat([]byte("metadata-only-copy-plaintext-"), 64*1024) + want := mustChecksum(t, hash.ChecksumCRC32, data) + object := "copy-metadata/existing-checksum.txt" + putCopyChecksumSource(t, apiRouter, credentials, bucketName, object, data, + map[string]string{xhttp.AmzChecksumCRC32: want}) + before, err := obj.GetObjectInfo(t.Context(), bucketName, object, ObjectOptions{}) + if err != nil || before.IsCompressed() { + t.Fatalf("%s: invalid metadata-copy precondition: compressed=%v size=%d err=%v", + instanceType, before.IsCompressed(), before.Size, err) + } + + restoreCompression := setCopyChecksumCompression(true) + compressionRestored := false + defer func() { + if !compressionRestored { + restoreCompression() + } + }() + + rec := copyChecksumRequest(t, apiRouter, credentials, bucketName, object, object, + map[string]string{xhttp.AmzMetadataDirective: "REPLACE"}) + if rec.Code != http.StatusOK { + t.Fatalf("%s: metadata-only CopyObject failed: %d %s", instanceType, rec.Code, rec.Body.String()) + } + assertCopyChecksum(t, obj, bucketName, object, hash.ChecksumCRC32, data, false, nil) + if got := readCopyChecksumObject(t, obj, bucketName, object, ObjectOptions{}); !bytes.Equal(got, data) { + prefix := got + if len(prefix) > 100 { + prefix = prefix[:100] + } + t.Fatalf("%s: metadata-only CopyObject body differs: got %d bytes, want %d, prefix %q", + instanceType, len(got), len(data), prefix) + } + afterMetadataCopy, err := obj.GetObjectInfo(t.Context(), bucketName, object, ObjectOptions{}) + if err != nil { + t.Fatal(err) + } + if before.VersionID != "" && afterMetadataCopy.VersionID == before.VersionID { + t.Fatalf("%s: versioned metadata-only copy did not create a new version", instanceType) + } + + destination := "copy-metadata/rewritten.txt" + rec = copyChecksumRequest(t, apiRouter, credentials, bucketName, object, destination, nil) + if rec.Code != http.StatusOK { + t.Fatalf("%s: data-rewriting CopyObject failed: %d %s", instanceType, rec.Code, rec.Body.String()) + } + assertCopyChecksum(t, obj, bucketName, destination, hash.ChecksumCRC32, data, true, nil) + + compressedObject := "copy-metadata/preserve-compressed.txt" + putCopyChecksumSource(t, apiRouter, credentials, bucketName, compressedObject, data, + map[string]string{xhttp.AmzChecksumCRC32: want}) + assertCopyChecksum(t, obj, bucketName, compressedObject, hash.ChecksumCRC32, data, true, nil) + + restoreCompression() + compressionRestored = true + rec = copyChecksumRequest(t, apiRouter, credentials, bucketName, compressedObject, compressedObject, + map[string]string{xhttp.AmzMetadataDirective: "REPLACE"}) + if rec.Code != http.StatusOK { + t.Fatalf("%s: compressed metadata-only CopyObject failed: %d %s", instanceType, rec.Code, rec.Body.String()) + } + assertCopyChecksum(t, obj, bucketName, compressedObject, hash.ChecksumCRC32, data, true, nil) + if got := readCopyChecksumObject(t, obj, bucketName, compressedObject, ObjectOptions{}); !bytes.Equal(got, data) { + t.Fatalf("%s: compressed metadata-only CopyObject body differs", instanceType) + } +} + +func TestAPICopyObjectSSECKeyRotationKeepsCompressionState(t *testing.T) { + defer DetectTestLeak(t)() + for _, versioned := range []bool{false, true} { + name := "unversioned" + if versioned { + name = "versioned" + } + t.Run(name, func(t *testing.T) { + ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{ + t: t, + objAPITest: testAPICopyObjectSSECKeyRotationKeepsCompressionState, + endpoints: []string{"CopyObject", "PutObject", "GetObject"}, + makeBucketOptions: MakeBucketOptions{VersioningEnabled: versioned}, + }) + }) + } +} + +func testAPICopyObjectSSECKeyRotationKeepsCompressionState(obj ObjectLayer, instanceType, bucketName string, + apiRouter http.Handler, credentials auth.Credentials, t *testing.T, +) { + previousTLS := globalIsTLS + globalIsTLS = true + defer func() { globalIsTLS = previousTLS }() + + data := bytes.Repeat([]byte("key-rotation-plaintext-"), 64*1024) + object := "copy-metadata/key-rotation.txt" + oldKey := bytes.Repeat([]byte{0x11}, 32) + oldMD5 := md5.Sum(oldKey) + newKey := bytes.Repeat([]byte{0x22}, 32) + newMD5 := md5.Sum(newKey) + + putCopyChecksumSource(t, apiRouter, credentials, bucketName, object, data, map[string]string{ + xhttp.AmzChecksumCRC32: mustChecksum(t, hash.ChecksumCRC32, data), + xhttp.AmzServerSideEncryptionCustomerAlgorithm: xhttp.AmzEncryptionAES, + xhttp.AmzServerSideEncryptionCustomerKey: base64.StdEncoding.EncodeToString(oldKey), + xhttp.AmzServerSideEncryptionCustomerKeyMD5: base64.StdEncoding.EncodeToString(oldMD5[:]), + }) + before, err := obj.GetObjectInfo(t.Context(), bucketName, object, ObjectOptions{}) + if err != nil || before.IsCompressed() { + t.Fatalf("%s: invalid key-rotation precondition: compressed=%v err=%v", instanceType, before.IsCompressed(), err) + } + + restoreCompression := setCopyChecksumCompression(true) + defer restoreCompression() + rec := copyChecksumRequest(t, apiRouter, credentials, bucketName, object, object, map[string]string{ + xhttp.AmzServerSideEncryptionCustomerAlgorithm: xhttp.AmzEncryptionAES, + xhttp.AmzServerSideEncryptionCustomerKey: base64.StdEncoding.EncodeToString(newKey), + xhttp.AmzServerSideEncryptionCustomerKeyMD5: base64.StdEncoding.EncodeToString(newMD5[:]), + xhttp.AmzServerSideEncryptionCopyCustomerAlgorithm: xhttp.AmzEncryptionAES, + xhttp.AmzServerSideEncryptionCopyCustomerKey: base64.StdEncoding.EncodeToString(oldKey), + xhttp.AmzServerSideEncryptionCopyCustomerKeyMD5: base64.StdEncoding.EncodeToString(oldMD5[:]), + }) + if rec.Code != http.StatusOK { + t.Fatalf("%s: key rotation failed: %d %s", instanceType, rec.Code, rec.Body.String()) + } + assertCopyChecksumResponse(t, rec, hash.ChecksumCRC32, data) + after, err := obj.GetObjectInfo(t.Context(), bucketName, object, ObjectOptions{}) + if err != nil { + t.Fatal(err) + } + if after.IsCompressed() { + t.Fatalf("%s: metadata-only key rotation stamped compression metadata", instanceType) + } + if before.VersionID != "" && after.VersionID == before.VersionID { + t.Fatalf("%s: versioned key rotation did not create a new version", instanceType) + } + + getHeaders := map[string]string{ + xhttp.AmzServerSideEncryptionCustomerAlgorithm: xhttp.AmzEncryptionAES, + xhttp.AmzServerSideEncryptionCustomerKey: base64.StdEncoding.EncodeToString(newKey), + xhttp.AmzServerSideEncryptionCustomerKeyMD5: base64.StdEncoding.EncodeToString(newMD5[:]), + } + req, err := newTestSignedRequestV4(http.MethodGet, getGetObjectURL("", bucketName, object), + 0, nil, credentials.AccessKey, credentials.SecretKey, getHeaders) + if err != nil { + t.Fatalf("failed to build GetObject request: %v", err) + } + response := httptest.NewRecorder() + apiRouter.ServeHTTP(response, req) + if response.Code != http.StatusOK || !bytes.Equal(response.Body.Bytes(), data) { + t.Fatalf("%s: post-rotation GetObject returned %d with %d bytes, want 200 with %d bytes: %s", + instanceType, response.Code, response.Body.Len(), len(data), response.Body.String()) + } +} + +// TestAPICopyObjectMetadataOnlyNullVersion covers the copy whose source is a +// null version on a bucket that gained versioning after the object was written. +// The object layer cannot reference such a version, so it rewrites the data and +// the recorded compression metadata has to describe the rewritten bytes. +func TestAPICopyObjectMetadataOnlyNullVersion(t *testing.T) { + defer DetectTestLeak(t)() + ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{ + t: t, + objAPITest: testAPICopyObjectMetadataOnlyNullVersion, + endpoints: []string{"CopyObject", "PutObject", "GetObject"}, + }) +} + +func testAPICopyObjectMetadataOnlyNullVersion(obj ObjectLayer, instanceType, bucketName string, + apiRouter http.Handler, credentials auth.Credentials, t *testing.T, +) { + restoreCompression := setCopyChecksumCompression(true) + compressionRestored := false + defer func() { + if !compressionRestored { + restoreCompression() + } + }() + + data := bytes.Repeat([]byte("null-version-metadata-copy-"), 64*1024) + want := mustChecksum(t, hash.ChecksumCRC32, data) + object := "copy-metadata/null-version.txt" + putCopyChecksumSource(t, apiRouter, credentials, bucketName, object, data, + map[string]string{xhttp.AmzChecksumCRC32: want}) + + before, err := obj.GetObjectInfo(t.Context(), bucketName, object, ObjectOptions{}) + if err != nil { + t.Fatal(err) + } + if !before.IsCompressed() || before.VersionID != "" { + t.Fatalf("%s: invalid null-version precondition: compressed=%v versionID=%q", + instanceType, before.IsCompressed(), before.VersionID) + } + + // Versioning is enabled after the write, so the object keeps a null version. + if _, err := globalBucketMetadataSys.Update(t.Context(), bucketName, + bucketVersioningConfig, enabledBucketVersioningConfig); err != nil { + t.Fatalf("%s: unable to enable versioning: %v", instanceType, err) + } + if !globalBucketVersioningSys.PrefixEnabled(bucketName, object) { + t.Fatalf("%s: versioning did not become enabled", instanceType) + } + + // Without compression the rewritten destination stores plaintext. + restoreCompression() + compressionRestored = true + + rec := copyChecksumRequest(t, apiRouter, credentials, bucketName, object, object, + map[string]string{xhttp.AmzMetadataDirective: "REPLACE"}) + if rec.Code != http.StatusOK { + t.Fatalf("%s: metadata-only CopyObject failed: %d %s", instanceType, rec.Code, rec.Body.String()) + } + + after := assertCopyChecksum(t, obj, bucketName, object, hash.ChecksumCRC32, data, false, nil) + if after.VersionID == "" { + t.Fatalf("%s: versioned copy did not create a new version", instanceType) + } + if got := readCopyChecksumObject(t, obj, bucketName, object, ObjectOptions{}); !bytes.Equal(got, data) { + t.Fatalf("%s: copied object body differs: got %d bytes, want %d", instanceType, len(got), len(data)) + } +} + +func TestAPICopyObjectMetadataOnlyNullVersionCompressesRewrite(t *testing.T) { + defer DetectTestLeak(t)() + ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{ + t: t, + objAPITest: testAPICopyObjectMetadataOnlyNullVersionCompressesRewrite, + endpoints: []string{"CopyObject", "PutObject", "GetObject"}, + }) +} + +func testAPICopyObjectMetadataOnlyNullVersionCompressesRewrite(obj ObjectLayer, instanceType, bucketName string, + apiRouter http.Handler, credentials auth.Credentials, t *testing.T, +) { + globalCompressConfigMu.Lock() + previousCompression := globalCompressConfig + globalCompressConfig.Enabled = false + globalCompressConfigMu.Unlock() + defer func() { + globalCompressConfigMu.Lock() + globalCompressConfig = previousCompression + globalCompressConfigMu.Unlock() + }() + + data := bytes.Repeat([]byte("null-version-compress-rewrite-"), 64*1024) + want := mustChecksum(t, hash.ChecksumCRC32, data) + object := "copy-metadata/null-version-compress.txt" + putCopyChecksumSource(t, apiRouter, credentials, bucketName, object, data, + map[string]string{xhttp.AmzChecksumCRC32: want}) + + before, err := obj.GetObjectInfo(t.Context(), bucketName, object, ObjectOptions{}) + if err != nil { + t.Fatal(err) + } + if before.IsCompressed() || before.VersionID != "" { + t.Fatalf("%s: invalid null-version precondition: compressed=%v versionID=%q", + instanceType, before.IsCompressed(), before.VersionID) + } + if _, err := globalBucketMetadataSys.Update(t.Context(), bucketName, + bucketVersioningConfig, enabledBucketVersioningConfig); err != nil { + t.Fatalf("%s: unable to enable versioning: %v", instanceType, err) + } + + restoreCopyCompression := setCopyChecksumCompression(false) + defer restoreCopyCompression() + rec := copyChecksumRequest(t, apiRouter, credentials, bucketName, object, object, + map[string]string{xhttp.AmzMetadataDirective: "REPLACE"}) + if rec.Code != http.StatusOK { + t.Fatalf("%s: metadata-only CopyObject failed: %d %s", instanceType, rec.Code, rec.Body.String()) + } + + after := assertCopyChecksum(t, obj, bucketName, object, hash.ChecksumCRC32, data, true, nil) + if after.VersionID == "" { + t.Fatalf("%s: versioned copy did not create a new version", instanceType) + } + if got := readCopyChecksumObject(t, obj, bucketName, object, ObjectOptions{}); !bytes.Equal(got, data) { + t.Fatalf("%s: copied object body differs: got %d bytes, want %d", instanceType, len(got), len(data)) + } +} + +func TestCopyRewritesObjectData(t *testing.T) { + tests := []struct { + name string + metadataOnly bool + srcOpts ObjectOptions + dstOpts ObjectOptions + want bool + }{ + { + name: "data copy always rewrites", + want: true, + }, + // PostRestoreObjectHandler, updateRestoreMetadata and batchKeyRotate all + // address the same version on both sides and never set Versioned, so they + // only ever reach these two cases. + { + name: "unversioned in-place metadata update", + metadataOnly: true, + }, + { + name: "addressed version updated in place", + metadataOnly: true, + srcOpts: ObjectOptions{VersionID: "v1"}, + dstOpts: ObjectOptions{VersionID: "v1"}, + }, + { + name: "versioned self referential version", + metadataOnly: true, + srcOpts: ObjectOptions{VersionID: "v1"}, + dstOpts: ObjectOptions{Versioned: true}, + }, + { + name: "versioned null source version cannot be referenced", + metadataOnly: true, + dstOpts: ObjectOptions{Versioned: true}, + want: true, + }, + { + name: "suspended destination with an addressed source version", + metadataOnly: true, + srcOpts: ObjectOptions{VersionID: "v1"}, + dstOpts: ObjectOptions{VersionSuspended: true, VersionID: nullVersionID}, + want: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := copyRewritesObjectData(tt.metadataOnly, tt.srcOpts, tt.dstOpts); got != tt.want { + t.Fatalf("copyRewritesObjectData() = %v, want %v", got, tt.want) + } + }) + } +} + +// TestAPICopyObjectSSECKeyRotationNullVersion covers an SSE-C key rotation whose +// source is a null version on a bucket that gained versioning after the object +// was written. A rotation only rewraps the object key held in metadata, so it +// may not take the metadata-only path when the object layer stores new object +// data; the rotation has to re-encrypt instead. +func TestAPICopyObjectSSECKeyRotationNullVersion(t *testing.T) { + defer DetectTestLeak(t)() + ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{ + t: t, + objAPITest: testAPICopyObjectSSECKeyRotationNullVersion, + endpoints: []string{"CopyObject", "PutObject", "GetObject"}, + }) +} + +func testAPICopyObjectSSECKeyRotationNullVersion(obj ObjectLayer, instanceType, bucketName string, + apiRouter http.Handler, credentials auth.Credentials, t *testing.T, +) { + testAPICopyObjectSSECKeyRotationNullVersionWithCompression(obj, instanceType, bucketName, + apiRouter, credentials, false, t) +} + +func TestAPICopyObjectSSECKeyRotationNullVersionSkipsCompression(t *testing.T) { + defer DetectTestLeak(t)() + ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{ + t: t, + objAPITest: testAPICopyObjectSSECKeyRotationNullVersionSkipsCompression, + endpoints: []string{"CopyObject", "PutObject", "GetObject"}, + }) +} + +func testAPICopyObjectSSECKeyRotationNullVersionSkipsCompression(obj ObjectLayer, instanceType, bucketName string, + apiRouter http.Handler, credentials auth.Credentials, t *testing.T, +) { + testAPICopyObjectSSECKeyRotationNullVersionWithCompression(obj, instanceType, bucketName, + apiRouter, credentials, true, t) +} + +func testAPICopyObjectSSECKeyRotationNullVersionWithCompression(obj ObjectLayer, instanceType, bucketName string, + apiRouter http.Handler, credentials auth.Credentials, compressAtCopy bool, t *testing.T, +) { + previousTLS := globalIsTLS + globalIsTLS = true + defer func() { globalIsTLS = previousTLS }() + + data := bytes.Repeat([]byte("key-rotation-null-version-"), 64*1024) + object := "copy-metadata/key-rotation-null.txt" + oldKey := bytes.Repeat([]byte{0x11}, 32) + oldMD5 := md5.Sum(oldKey) + newKey := bytes.Repeat([]byte{0x22}, 32) + newMD5 := md5.Sum(newKey) + + putCopyChecksumSource(t, apiRouter, credentials, bucketName, object, data, map[string]string{ + xhttp.AmzChecksumCRC32: mustChecksum(t, hash.ChecksumCRC32, data), + xhttp.AmzServerSideEncryptionCustomerAlgorithm: xhttp.AmzEncryptionAES, + xhttp.AmzServerSideEncryptionCustomerKey: base64.StdEncoding.EncodeToString(oldKey), + xhttp.AmzServerSideEncryptionCustomerKeyMD5: base64.StdEncoding.EncodeToString(oldMD5[:]), + }) + before, err := obj.GetObjectInfo(t.Context(), bucketName, object, ObjectOptions{}) + if err != nil { + t.Fatal(err) + } + if before.VersionID != "" { + t.Fatalf("%s: invalid null-version precondition: versionID=%q", instanceType, before.VersionID) + } + + // Versioning is enabled after the write, so the object keeps a null version. + if _, err := globalBucketMetadataSys.Update(t.Context(), bucketName, + bucketVersioningConfig, enabledBucketVersioningConfig); err != nil { + t.Fatalf("%s: unable to enable versioning: %v", instanceType, err) + } + if !globalBucketVersioningSys.PrefixEnabled(bucketName, object) { + t.Fatalf("%s: versioning did not become enabled", instanceType) + } + if compressAtCopy { + restoreCompression := setCopyChecksumCompression(true) + defer restoreCompression() + } + + rec := copyChecksumRequest(t, apiRouter, credentials, bucketName, object, object, map[string]string{ + xhttp.AmzServerSideEncryptionCustomerAlgorithm: xhttp.AmzEncryptionAES, + xhttp.AmzServerSideEncryptionCustomerKey: base64.StdEncoding.EncodeToString(newKey), + xhttp.AmzServerSideEncryptionCustomerKeyMD5: base64.StdEncoding.EncodeToString(newMD5[:]), + xhttp.AmzServerSideEncryptionCopyCustomerAlgorithm: xhttp.AmzEncryptionAES, + xhttp.AmzServerSideEncryptionCopyCustomerKey: base64.StdEncoding.EncodeToString(oldKey), + xhttp.AmzServerSideEncryptionCopyCustomerKeyMD5: base64.StdEncoding.EncodeToString(oldMD5[:]), + }) + if rec.Code != http.StatusOK { + t.Fatalf("%s: key rotation failed: %d %s", instanceType, rec.Code, rec.Body.String()) + } + + assertCopyChecksumResponse(t, rec, hash.ChecksumCRC32, data) + + getHeaders := map[string]string{ + xhttp.AmzServerSideEncryptionCustomerAlgorithm: xhttp.AmzEncryptionAES, + xhttp.AmzServerSideEncryptionCustomerKey: base64.StdEncoding.EncodeToString(newKey), + xhttp.AmzServerSideEncryptionCustomerKeyMD5: base64.StdEncoding.EncodeToString(newMD5[:]), + } + req, err := newTestSignedRequestV4(http.MethodGet, getGetObjectURL("", bucketName, object), + 0, nil, credentials.AccessKey, credentials.SecretKey, getHeaders) + if err != nil { + t.Fatalf("failed to build GetObject request: %v", err) + } + response := httptest.NewRecorder() + apiRouter.ServeHTTP(response, req) + if response.Code != http.StatusOK || !bytes.Equal(response.Body.Bytes(), data) { + t.Fatalf("%s: post-rotation GetObject returned %d with %d bytes, want 200 with %d bytes: %s", + instanceType, response.Code, response.Body.Len(), len(data), response.Body.String()) + } + + decryptHeaders := http.Header{} + for key, value := range getHeaders { + decryptHeaders.Set(key, value) + } + // This SSE-C rewrite stays uncompressed even with compression enabled at copy + // time. The plaintext sibling + // TestAPICopyObjectMetadataOnlyNullVersionCompressesRewrite keeps the + // coverage that a compressed rewrite records matching metadata. + after := assertCopyChecksum(t, obj, bucketName, object, hash.ChecksumCRC32, data, false, decryptHeaders) + if after.VersionID == "" { + t.Fatalf("%s: rotation into a versioned bucket did not create a new version", instanceType) + } + // The rotation could not be applied in place, so the object was re-encrypted + // under a fresh object key. That regenerates the encrypted ETag, unlike an + // in-place rotation which leaves the stored bytes and the ETag alone. + if after.ETag == before.ETag { + t.Fatalf("%s: re-encrypting rotation kept the source ETag %q", instanceType, after.ETag) + } +} + +// TestAPICopyObjectSSECKeyRotationNullVersionWrongKey pins source-key +// authentication in both the standalone rotation fix and the later zero-byte +// read hardening. +func TestAPICopyObjectSSECKeyRotationNullVersionWrongKey(t *testing.T) { + defer DetectTestLeak(t)() + ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{ + t: t, + objAPITest: testAPICopyObjectSSECKeyRotationNullVersionWrongKey, + endpoints: []string{"CopyObject", "PutObject", "GetObject"}, + }) +} + +func testAPICopyObjectSSECKeyRotationNullVersionWrongKey(obj ObjectLayer, instanceType, bucketName string, + apiRouter http.Handler, credentials auth.Credentials, t *testing.T, +) { + previousTLS := globalIsTLS + globalIsTLS = true + defer func() { globalIsTLS = previousTLS }() + + object := "copy-metadata/key-rotation-null-empty.txt" + oldKey := bytes.Repeat([]byte{0x11}, 32) + oldMD5 := md5.Sum(oldKey) + wrongKey := bytes.Repeat([]byte{0x33}, 32) + wrongMD5 := md5.Sum(wrongKey) + newKey := bytes.Repeat([]byte{0x22}, 32) + newMD5 := md5.Sum(newKey) + + putCopyChecksumSource(t, apiRouter, credentials, bucketName, object, nil, map[string]string{ + xhttp.AmzServerSideEncryptionCustomerAlgorithm: xhttp.AmzEncryptionAES, + xhttp.AmzServerSideEncryptionCustomerKey: base64.StdEncoding.EncodeToString(oldKey), + xhttp.AmzServerSideEncryptionCustomerKeyMD5: base64.StdEncoding.EncodeToString(oldMD5[:]), + }) + before, err := obj.GetObjectInfo(t.Context(), bucketName, object, ObjectOptions{}) + if err != nil { + t.Fatal(err) + } + if before.Size != 0 || before.VersionID != "" || len(before.Checksum) != 0 { + t.Fatalf("%s: invalid empty null-version precondition: size=%d versionID=%q checksum=%d", + instanceType, before.Size, before.VersionID, len(before.Checksum)) + } + + if _, err := globalBucketMetadataSys.Update(t.Context(), bucketName, + bucketVersioningConfig, enabledBucketVersioningConfig); err != nil { + t.Fatalf("%s: unable to enable versioning: %v", instanceType, err) + } + + rec := copyChecksumRequest(t, apiRouter, credentials, bucketName, object, object, map[string]string{ + xhttp.AmzServerSideEncryptionCustomerAlgorithm: xhttp.AmzEncryptionAES, + xhttp.AmzServerSideEncryptionCustomerKey: base64.StdEncoding.EncodeToString(newKey), + xhttp.AmzServerSideEncryptionCustomerKeyMD5: base64.StdEncoding.EncodeToString(newMD5[:]), + xhttp.AmzServerSideEncryptionCopyCustomerAlgorithm: xhttp.AmzEncryptionAES, + xhttp.AmzServerSideEncryptionCopyCustomerKey: base64.StdEncoding.EncodeToString(wrongKey), + xhttp.AmzServerSideEncryptionCopyCustomerKeyMD5: base64.StdEncoding.EncodeToString(wrongMD5[:]), + }) + if rec.Code != http.StatusForbidden { + t.Fatalf("%s: rotation with an incorrect source key returned %d, want %d: %s", + instanceType, rec.Code, http.StatusForbidden, rec.Body.String()) + } + + rec = copyChecksumRequest(t, apiRouter, credentials, bucketName, object, object, map[string]string{ + xhttp.AmzServerSideEncryptionCustomerAlgorithm: xhttp.AmzEncryptionAES, + xhttp.AmzServerSideEncryptionCustomerKey: base64.StdEncoding.EncodeToString(newKey), + xhttp.AmzServerSideEncryptionCustomerKeyMD5: base64.StdEncoding.EncodeToString(newMD5[:]), + xhttp.AmzServerSideEncryptionCopyCustomerAlgorithm: xhttp.AmzEncryptionAES, + xhttp.AmzServerSideEncryptionCopyCustomerKey: base64.StdEncoding.EncodeToString(newKey), + xhttp.AmzServerSideEncryptionCopyCustomerKeyMD5: base64.StdEncoding.EncodeToString(newMD5[:]), + }) + // The zero-byte read path authenticates the source key before the + // rotation-specific equal-key distinction, matching non-empty reads. + if rec.Code != http.StatusForbidden { + t.Fatalf("%s: rotation with equal invalid keys returned %d, want %d: %s", + instanceType, rec.Code, http.StatusForbidden, rec.Body.String()) + } + + after, err := obj.GetObjectInfo(t.Context(), bucketName, object, ObjectOptions{}) + if err != nil { + t.Fatal(err) + } + if after.VersionID != "" { + t.Fatalf("%s: rejected rotation still created version %q", instanceType, after.VersionID) + } + + // The object stays readable with the key it was written under. + req, err := newTestSignedRequestV4(http.MethodGet, getGetObjectURL("", bucketName, object), + 0, nil, credentials.AccessKey, credentials.SecretKey, map[string]string{ + xhttp.AmzServerSideEncryptionCustomerAlgorithm: xhttp.AmzEncryptionAES, + xhttp.AmzServerSideEncryptionCustomerKey: base64.StdEncoding.EncodeToString(oldKey), + xhttp.AmzServerSideEncryptionCustomerKeyMD5: base64.StdEncoding.EncodeToString(oldMD5[:]), + }) + if err != nil { + t.Fatalf("failed to build GetObject request: %v", err) + } + response := httptest.NewRecorder() + apiRouter.ServeHTTP(response, req) + if response.Code != http.StatusOK || response.Body.Len() != 0 { + t.Fatalf("%s: original object no longer readable: %d with %d bytes: %s", + instanceType, response.Code, response.Body.Len(), response.Body.String()) + } +} diff --git a/cmd/object-crc64-composite_test.go b/cmd/object-crc64-composite_test.go new file mode 100644 index 000000000..70a80370d --- /dev/null +++ b/cmd/object-crc64-composite_test.go @@ -0,0 +1,79 @@ +// Copyright (c) 2015-2026 MinIO, Inc. +// +// This file is part of MinIO Object Storage stack +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package cmd + +import ( + "bytes" + "net/http" + "net/http/httptest" + "testing" + + "github.com/minio/minio/internal/auth" + "github.com/minio/minio/internal/hash" + xhttp "github.com/minio/minio/internal/http" +) + +func TestAPIPutObjectRejectsCRC64Composite(t *testing.T) { + defer DetectTestLeak(t)() + ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{ + t: t, + objAPITest: testAPIPutObjectRejectsCRC64Composite, + endpoints: []string{"PutObject"}, + }) +} + +func testAPIPutObjectRejectsCRC64Composite(obj ObjectLayer, instanceType, bucketName string, + apiRouter http.Handler, credentials auth.Credentials, t *testing.T, +) { + data := []byte("crc64-composite") + object := "checksums/crc64-composite" + req, err := newTestSignedRequestV4(http.MethodPut, getPutObjectURL("", bucketName, object), + int64(len(data)), bytes.NewReader(data), credentials.AccessKey, credentials.SecretKey, map[string]string{ + xhttp.AmzChecksumCRC64NVME: mustChecksum(t, hash.ChecksumCRC64NVME, data), + xhttp.AmzChecksumType: xhttp.AmzChecksumTypeComposite, + }) + if err != nil { + t.Fatal(err) + } + rec := httptest.NewRecorder() + apiRouter.ServeHTTP(rec, req) + if rec.Code != http.StatusBadRequest || apiErrorCode(t, rec) != "InvalidArgument" { + t.Fatalf("%s: CRC64NVME/COMPOSITE PutObject returned %d %s", instanceType, rec.Code, rec.Body.String()) + } + if _, err := obj.GetObjectInfo(t.Context(), bucketName, object, ObjectOptions{}); !isErrObjectNotFound(err) { + t.Fatalf("%s: rejected PutObject stored an object: %v", instanceType, err) + } + + trailerObject := "checksums/crc64-composite-trailer" + req, err = newTestSignedRequestV4(http.MethodPut, getPutObjectURL("", bucketName, trailerObject), + int64(len(data)), bytes.NewReader(data), credentials.AccessKey, credentials.SecretKey, map[string]string{ + xhttp.AmzTrailer: xhttp.AmzChecksumCRC64NVME, + xhttp.AmzChecksumType: xhttp.AmzChecksumTypeComposite, + }) + if err != nil { + t.Fatal(err) + } + rec = httptest.NewRecorder() + apiRouter.ServeHTTP(rec, req) + if rec.Code != http.StatusBadRequest || apiErrorCode(t, rec) != "InvalidArgument" { + t.Fatalf("%s: trailing CRC64NVME/COMPOSITE PutObject returned %d %s", instanceType, rec.Code, rec.Body.String()) + } + if _, err := obj.GetObjectInfo(t.Context(), bucketName, trailerObject, ObjectOptions{}); !isErrObjectNotFound(err) { + t.Fatalf("%s: rejected trailing PutObject stored an object: %v", instanceType, err) + } +} diff --git a/cmd/object-handlers-chunked-checksum_test.go b/cmd/object-handlers-chunked-checksum_test.go new file mode 100644 index 000000000..abd9f0d16 --- /dev/null +++ b/cmd/object-handlers-chunked-checksum_test.go @@ -0,0 +1,138 @@ +// Copyright (c) 2015-2026 MinIO, Inc. +// +// This file is part of MinIO Object Storage stack +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package cmd + +import ( + "bytes" + "encoding/base64" + "encoding/binary" + "encoding/xml" + "hash/crc32" + "io" + "net/http" + "net/http/httptest" + "testing" + + "github.com/minio/minio/internal/auth" +) + +func crc32Checksum(data []byte) string { + var c [4]byte + binary.BigEndian.PutUint32(c[:], crc32.ChecksumIEEE(data)) + return base64.StdEncoding.EncodeToString(c[:]) +} + +// TestAPIPutObjectChunkedChecksum exercises PutObject with aws-chunked streaming +// transfer encoding combined with a CRC32 checksum. It reproduces issue #107: +// the AWS Java SDK v2, with chunked encoding enabled, sends a non-trailer signed +// chunked body (x-amz-content-sha256: STREAMING-AWS4-HMAC-SHA256-PAYLOAD), places +// the precomputed checksum in the x-amz-checksum-crc32 header, yet still advertises +// it in x-amz-trailer even though no trailer is ever sent. Before the fix the +// server treated the checksum as trailing (empty value) and returned HTTP 400 +// XAmzContentChecksumMismatch. +func TestAPIPutObjectChunkedChecksum(t *testing.T) { + defer DetectTestLeak(t)() + ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{ + t: t, + objAPITest: testAPIPutObjectChunkedChecksum, + endpoints: []string{"PutObject", "GetObject"}, + }) +} + +func testAPIPutObjectChunkedChecksum(obj ObjectLayer, instanceType, bucketName string, apiRouter http.Handler, + credentials auth.Credentials, t *testing.T, +) { + data := bytes.Repeat([]byte("a"), 4096) + goodCRC := crc32Checksum(data) + // A well-formed 4-byte value that does not match the content. + wrongCRC := base64.StdEncoding.EncodeToString([]byte{0x01, 0x02, 0x03, 0x04}) + + apiCode := func(rec *httptest.ResponseRecorder) string { + var apiErr APIErrorResponse + b, _ := io.ReadAll(rec.Body) + _ = xml.Unmarshal(b, &apiErr) + return apiErr.Code + } + + // newChunkedJavaForm builds a non-trailer signed chunked PutObject request that + // mirrors the AWS Java SDK v2 wire form: the checksum value sits in the header + // while x-amz-trailer still advertises it (no trailer is actually sent). The + // checksum headers are set BEFORE signing so they are part of the signed + // headers, exactly as the captured Java SDK request sends them. + newChunkedJavaForm := func(object, crc string) *http.Request { + body := bytes.NewReader(data) + req, err := newTestStreamingRequest(http.MethodPut, + getPutObjectURL("", bucketName, object), + int64(len(data)), int64(len(data)), body) + if err != nil { + t.Fatalf("Failed to create streaming request: %v", err) + } + req.Header.Set("x-amz-checksum-crc32", crc) + req.Header.Set("x-amz-trailer", "x-amz-checksum-crc32") + req.Header.Set("x-amz-sdk-checksum-algorithm", "CRC32") + currTime := UTCNow() + signature, err := signStreamingRequest(req, credentials.AccessKey, credentials.SecretKey, currTime) + if err != nil { + t.Fatalf("Failed to sign streaming request: %v", err) + } + req, err = assembleStreamingChunks(req, body, int64(len(data)), credentials.SecretKey, signature, currTime) + if err != nil { + t.Fatalf("Failed to assemble streaming chunks: %v", err) + } + return req + } + + // 1. Correct checksum: must succeed and echo the checksum on the response. + { + rec := httptest.NewRecorder() + apiRouter.ServeHTTP(rec, newChunkedJavaForm("chunked-ok", goodCRC)) + if rec.Code != http.StatusOK { + t.Fatalf("%s: chunked+CRC32 PutObject: expected 200, got %d (%s)", instanceType, rec.Code, apiCode(rec)) + } + if got := rec.Header().Get("x-amz-checksum-crc32"); got != goodCRC { + t.Fatalf("%s: response checksum echo = %q, want %q", instanceType, got, goodCRC) + } + + // Read the stored checksum back via GetObject with checksum mode enabled. + greq, err := newTestSignedRequestV4(http.MethodGet, getGetObjectURL("", bucketName, "chunked-ok"), + 0, nil, credentials.AccessKey, credentials.SecretKey, map[string]string{"x-amz-checksum-mode": "ENABLED"}) + if err != nil { + t.Fatalf("Failed to create GET request: %v", err) + } + grec := httptest.NewRecorder() + apiRouter.ServeHTTP(grec, greq) + if grec.Code != http.StatusOK { + t.Fatalf("%s: GetObject: expected 200, got %d", instanceType, grec.Code) + } + if got := grec.Header().Get("x-amz-checksum-crc32"); got != goodCRC { + t.Fatalf("%s: stored checksum read back = %q, want %q", instanceType, got, goodCRC) + } + } + + // 2. Wrong checksum over the same chunked form: must still be rejected. + { + rec := httptest.NewRecorder() + apiRouter.ServeHTTP(rec, newChunkedJavaForm("chunked-wrong", wrongCRC)) + if rec.Code != http.StatusBadRequest { + t.Fatalf("%s: chunked+wrong CRC32: expected 400, got %d", instanceType, rec.Code) + } + if code := apiCode(rec); code != "XAmzContentChecksumMismatch" { + t.Fatalf("%s: chunked+wrong CRC32: want XAmzContentChecksumMismatch, got %q", instanceType, code) + } + } +} diff --git a/cmd/object-handlers-common.go b/cmd/object-handlers-common.go index a6febc122..f8de11ab8 100644 --- a/cmd/object-handlers-common.go +++ b/cmd/object-handlers-common.go @@ -28,6 +28,7 @@ import ( "github.com/minio/minio/internal/amztime" "github.com/minio/minio/internal/bucket/lifecycle" + "github.com/minio/minio/internal/crypto" "github.com/minio/minio/internal/event" "github.com/minio/minio/internal/hash" xhttp "github.com/minio/minio/internal/http" @@ -193,7 +194,15 @@ func checkPreconditionsPUT(ctx context.Context, w http.ResponseWriter, r *http.R etagMatch := opts.PreserveETag != "" && isETagEqual(objInfo.ETag, opts.PreserveETag) vidMatch := opts.VersionID != "" && opts.VersionID == objInfo.VersionID - if etagMatch && vidMatch { + // A matching version and ETag normally mean the destination already holds + // this version, so the write is skipped. They do not establish that for an + // authenticated SSE-C replica write: the destination cannot decrypt or + // re-encrypt the body without the customer key, so it cannot verify the + // replica, and this retransmission is how such a replica is repaired or + // updated. The predicate is the incoming request's restored SSE-C metadata, + // not what the destination happens to hold. + ssecReplica := isReplicaTrusted(r.Context()) && crypto.SSEC.IsEncrypted(opts.UserDefined) + if etagMatch && vidMatch && !ssecReplica { writeHeaders() writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrPreconditionFailed), r.URL) return true @@ -340,6 +349,28 @@ func canonicalizeETag(etag string) string { return etagRegex.ReplaceAllString(etag, "$1") } +// deleteIfMatchPreconditionFailed reports whether the If-Match precondition on a +// DeleteObject request fails, in which case the delete must be refused with 412. +// It is pure and never writes to the ResponseWriter: DeleteObject may evaluate +// the precondition off the request goroutine (e.g. during multi-pool cleanup). +// +// - A delete marker (a non-live latest version) has no entity-tag to match, so +// any If-Match value, including "*", fails against it. +// - "*" matches any existing live object, so it only requires existence. +// - A concrete ETag is compared against the object's public ETag. For +// SSE-C/SSE-KMS objects the public ETag is derived from the stored suffix +// without the customer key (getDecryptedETag), so a satisfiable condition is +// never rejected merely because the caller did not supply the key. +func deleteIfMatchPreconditionFailed(h http.Header, ifMatch string, oi ObjectInfo) bool { + if oi.DeleteMarker { + return true + } + if strings.TrimSpace(ifMatch) == "*" { + return false + } + return !isETagEqual(getDecryptedETag(h, oi, false), ifMatch) +} + // isETagEqual return true if the canonical representations of two ETag strings // are equal, false otherwise func isETagEqual(left, right string) bool { @@ -353,6 +384,11 @@ func isETagEqual(left, right string) bool { // upon a success Put/Copy/CompleteMultipart/Delete requests // to activate delete only headers set delete as true func setPutObjHeaders(w http.ResponseWriter, objInfo ObjectInfo, del bool, h http.Header) { + cs, _ := objInfo.decryptChecksums(0, h) + setPutObjHeadersWithChecksum(w, objInfo, del, cs) +} + +func setPutObjHeadersWithChecksum(w http.ResponseWriter, objInfo ObjectInfo, del bool, cs map[string]string) { // We must not use the http.Header().Set method here because some (broken) // clients expect the ETag header key to be literally "ETag" - not "Etag" (case-sensitive). // Therefore, we have to set the ETag directly as map entry. @@ -374,7 +410,6 @@ func setPutObjHeaders(w http.ResponseWriter, objInfo ObjectInfo, del bool, h htt lc.SetPredictionHeaders(w, objInfo.ToLifecycleOpts()) } } - cs, _ := objInfo.decryptChecksums(0, h) hash.AddChecksumHeader(w, cs) } diff --git a/cmd/object-handlers-conditional-delete_test.go b/cmd/object-handlers-conditional-delete_test.go new file mode 100644 index 000000000..342a81791 --- /dev/null +++ b/cmd/object-handlers-conditional-delete_test.go @@ -0,0 +1,146 @@ +// Copyright (c) 2015-2025 MinIO, Inc. +// +// This file is part of MinIO Object Storage stack +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package cmd + +import ( + "bytes" + "context" + "encoding/xml" + "net/http" + "net/http/httptest" + "testing" + + "github.com/dustin/go-humanize" + "github.com/minio/minio/internal/auth" + xhttp "github.com/minio/minio/internal/http" +) + +// TestAPIDeleteObjectHandlerIfMatch verifies conditional DeleteObject behavior +// for the If-Match request header (AWS S3 conditional deletes): +// - a non-matching ETag must return 412 Precondition Failed and preserve the object, +// - a matching ETag (or "*") must delete the object and return 204, +// - a request without If-Match must be unaffected, +// - If-Match against a missing key must return 404 NoSuchKey (not the idempotent 204). +func TestAPIDeleteObjectHandlerIfMatch(t *testing.T) { + defer DetectTestLeak(t)() + ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{t: t, objAPITest: testAPIDeleteObjectHandlerIfMatch, endpoints: []string{"DeleteObject"}}) +} + +func testAPIDeleteObjectHandlerIfMatch(obj ObjectLayer, instanceType, bucketName string, apiRouter http.Handler, + credentials auth.Credentials, t *testing.T, +) { + // putObject (re)creates an object and returns its ETag. + putObject := func(object string) string { + data := generateBytesData(1 * humanize.MiByte) + oi, err := obj.PutObject(context.Background(), bucketName, object, + mustGetPutObjReader(t, bytes.NewReader(data), int64(len(data)), "", ""), ObjectOptions{}) + if err != nil { + t.Fatalf("%s: failed to put object %q: %v", instanceType, object, err) + } + return oi.ETag + } + + // exists reports whether the object is still present. + exists := func(object string) bool { + _, err := obj.GetObjectInfo(context.Background(), bucketName, object, ObjectOptions{}) + return err == nil + } + + // doDelete issues a signed DELETE, optionally with an If-Match header. + doDelete := func(object, ifMatch string) *httptest.ResponseRecorder { + var hdrs map[string]string + if ifMatch != "" { + hdrs = map[string]string{xhttp.IfMatch: ifMatch} + } + req, err := newTestSignedRequestV4(http.MethodDelete, getDeleteObjectURL("", bucketName, object), + 0, nil, credentials.AccessKey, credentials.SecretKey, hdrs) + if err != nil { + t.Fatalf("%s: failed to create DELETE request: %v", instanceType, err) + } + rec := httptest.NewRecorder() + apiRouter.ServeHTTP(rec, req) + return rec + } + + // (1) If-Match with a wrong ETag: 412 Precondition Failed, object preserved. + t.Run("wrong-etag-412", func(t *testing.T) { + object := "precond-wrong-etag" + putObject(object) + rec := doDelete(object, `"non-matching-etag"`) + if rec.Code != http.StatusPreconditionFailed { + t.Fatalf("%s: expected %d, got %d (body: %s)", instanceType, http.StatusPreconditionFailed, rec.Code, rec.Body.String()) + } + if !exists(object) { + t.Fatalf("%s: object must still exist after a failed conditional delete", instanceType) + } + }) + + // (2) If-Match with the correct ETag: 204 No Content, object removed. + t.Run("correct-etag-204", func(t *testing.T) { + object := "precond-correct-etag" + etag := putObject(object) + rec := doDelete(object, `"`+etag+`"`) + if rec.Code != http.StatusNoContent { + t.Fatalf("%s: expected %d, got %d (body: %s)", instanceType, http.StatusNoContent, rec.Code, rec.Body.String()) + } + if exists(object) { + t.Fatalf("%s: object must be removed after a matching conditional delete", instanceType) + } + }) + + // (3) If-Match "*" on an existing object matches any ETag: 204, object removed. + t.Run("wildcard-204", func(t *testing.T) { + object := "precond-wildcard" + putObject(object) + rec := doDelete(object, "*") + if rec.Code != http.StatusNoContent { + t.Fatalf("%s: expected %d, got %d (body: %s)", instanceType, http.StatusNoContent, rec.Code, rec.Body.String()) + } + if exists(object) { + t.Fatalf("%s: object must be removed after a wildcard conditional delete", instanceType) + } + }) + + // (4) No If-Match header: unchanged behavior, 204, object removed. + t.Run("no-ifmatch-204", func(t *testing.T) { + object := "precond-none" + putObject(object) + rec := doDelete(object, "") + if rec.Code != http.StatusNoContent { + t.Fatalf("%s: expected %d, got %d (body: %s)", instanceType, http.StatusNoContent, rec.Code, rec.Body.String()) + } + if exists(object) { + t.Fatalf("%s: object must be removed after an unconditional delete", instanceType) + } + }) + + // (5) If-Match against a non-existent key: 404 NoSuchKey, not the idempotent 204. + t.Run("missing-key-404", func(t *testing.T) { + rec := doDelete("precond-missing-key", `"some-etag"`) + if rec.Code != http.StatusNotFound { + t.Fatalf("%s: expected %d, got %d (body: %s)", instanceType, http.StatusNotFound, rec.Code, rec.Body.String()) + } + var apiErr APIErrorResponse + if err := xml.Unmarshal(rec.Body.Bytes(), &apiErr); err != nil { + t.Fatalf("%s: failed to parse error response: %v", instanceType, err) + } + if apiErr.Code != "NoSuchKey" { + t.Fatalf("%s: expected error code NoSuchKey, got %q", instanceType, apiErr.Code) + } + }) +} diff --git a/cmd/object-handlers-delete-precond_test.go b/cmd/object-handlers-delete-precond_test.go new file mode 100644 index 000000000..53c80a39c --- /dev/null +++ b/cmd/object-handlers-delete-precond_test.go @@ -0,0 +1,64 @@ +// Copyright (c) 2015-2025 MinIO, Inc. +// +// This file is part of MinIO Object Storage stack +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package cmd + +import ( + "net/http" + "testing" + + "github.com/minio/minio/internal/crypto" +) + +// TestDeleteIfMatchPreconditionFailed exercises the pure If-Match evaluation for +// DeleteObject, including delete markers, the "*" wildcard, and SSE-C objects +// whose public ETag must be derived without the customer key. +func TestDeleteIfMatchPreconditionFailed(t *testing.T) { + const plainETag = "d41d8cd98f00b204e9800998ecf8427e" // 32-char MD5, returned as-is + + // SSE-C object: the stored ETag is longer than 32 chars and its public form + // is the trailing 32 chars, derived by getDecryptedETag without any key. + ssecPublic := "11112222333344445555666677778888" + ssecStored := "abcdef0123456789abcdef0123456789" + ssecPublic // 64 chars + ssecMeta := map[string]string{crypto.MetaSealedKeySSEC: "test-sealed-key"} + + testCases := []struct { + name string + ifMatch string + oi ObjectInfo + want bool // true => precondition failed (delete refused, 412) + }{ + {"live matching etag", plainETag, ObjectInfo{ETag: plainETag}, false}, + {"live matching quoted etag", `"` + plainETag + `"`, ObjectInfo{ETag: plainETag}, false}, + {"live non-matching etag", "0badbeef0badbeef0badbeef0badbeef", ObjectInfo{ETag: plainETag}, true}, + {"live wildcard", "*", ObjectInfo{ETag: plainETag}, false}, + {"live wildcard padded", " * ", ObjectInfo{ETag: plainETag}, false}, + {"delete marker concrete etag", plainETag, ObjectInfo{DeleteMarker: true}, true}, + {"delete marker wildcard", "*", ObjectInfo{DeleteMarker: true}, true}, + {"ssec matching public etag", ssecPublic, ObjectInfo{ETag: ssecStored, UserDefined: ssecMeta}, false}, + {"ssec wildcard", "*", ObjectInfo{ETag: ssecStored, UserDefined: ssecMeta}, false}, + {"ssec non-matching etag", "99998888777766665555444433332222", ObjectInfo{ETag: ssecStored, UserDefined: ssecMeta}, true}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + if got := deleteIfMatchPreconditionFailed(http.Header{}, tc.ifMatch, tc.oi); got != tc.want { + t.Errorf("deleteIfMatchPreconditionFailed(%q) = %v, want %v", tc.ifMatch, got, tc.want) + } + }) + } +} diff --git a/cmd/object-handlers.go b/cmd/object-handlers.go index 4639833c9..1704bc54c 100644 --- a/cmd/object-handlers.go +++ b/cmd/object-handlers.go @@ -62,7 +62,8 @@ import ( "github.com/minio/minio/internal/logger" "github.com/minio/minio/internal/s3select" "github.com/minio/mux" - "github.com/minio/pkg/v3/policy" + "github.com/minio/sio" + "github.com/pgsty/silo-pkg/v3/policy" ) // supportedHeadGetReqParams - supported request parameters for GET and HEAD presigned request. @@ -356,6 +357,18 @@ func (api objectAPIHandlers) getObjectHandler(ctx context.Context, objectAPI Obj writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL) return } + if hasReplicationMarkerHeader(r.Header) { + trusted := hasReplicationMarker(r.Header) && + replicationPermissionAllowed(ctx, r, bucket, object, policy.ReplicateObjectAction) + ctx, r = applyReplicationTrust(ctx, r, trusted, trusted) + if trusted { + opts, err = getOpts(ctx, r, bucket, object) + if err != nil { + writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL) + return + } + } + } getObjectNInfo := objectAPI.GetObjectNInfo @@ -494,8 +507,8 @@ func (api objectAPIHandlers) getObjectHandler(ctx context.Context, objectAPI Obj } // filter object lock metadata if permission does not permit - getRetPerms := checkRequestAuthType(ctx, r, policy.GetObjectRetentionAction, bucket, object) - legalHoldPerms := checkRequestAuthType(ctx, r, policy.GetObjectLegalHoldAction, bucket, object) + getRetPerms := authorizeRequest(ctx, r, policy.GetObjectRetentionAction) + legalHoldPerms := authorizeRequest(ctx, r, policy.GetObjectLegalHoldAction) // filter object lock metadata if permission does not permit objInfo.UserDefined = objectlock.FilterObjectLockMetadata(objInfo.UserDefined, getRetPerms != ErrNone, legalHoldPerms != ErrNone) @@ -601,10 +614,16 @@ func (api objectAPIHandlers) getObjectAttributesHandler(ctx context.Context, obj writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL) return } + if hasReplicationMarkerHeader(r.Header) { + trusted := hasReplicationMarker(r.Header) && + replicationPermissionAllowed(ctx, r, bucket, object, policy.ReplicateObjectAction) + ctx, r = applyReplicationTrust(ctx, r, trusted, trusted) + opts.ReplicationRequest = trusted + } objInfo, err := objectAPI.GetObjectInfo(ctx, bucket, object, opts) if err != nil { - s3Error = checkRequestAuthType(ctx, r, policy.ListBucketAction, bucket, object) + s3Error = authorizeRequest(ctx, r, policy.ListBucketAction) if s3Error == ErrNone { writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL) return @@ -621,6 +640,15 @@ func (api objectAPIHandlers) getObjectAttributesHandler(ctx context.Context, obj if checkPreconditions(ctx, w, r, objInfo, opts) { return } + // Only a caller authorized to replicate this object may read SSE-C + // attributes without presenting the customer key. The header alone is + // client controlled, so it cannot stand in for that authorization. + if crypto.SSEC.IsEncrypted(objInfo.UserDefined) && !isReplicaTrusted(ctx) { + if _, err = crypto.SSEC.UnsealObjectKey(r.Header, objInfo.UserDefined, bucket, object); err != nil { + writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL) + return + } + } OA := new(getObjectAttributesResponse) @@ -662,6 +690,20 @@ func (api objectAPIHandlers) getObjectAttributesHandler(ctx context.Context, obj objInfo.decryptPartsChecksums(r.Header) if _, ok := opts.ObjectAttributes[xhttp.ObjectParts]; ok { + // Report each part's uploaded plaintext byte length. Parts are stored + // transformed, compressed and/or encrypted, so the stored size is not + // what AWS defines ObjectPart.Size to be. + _, isEncrypted := crypto.IsEncrypted(objInfo.UserDefined) + isCompressed := objInfo.IsCompressed() + // Only a part of an encrypted multipart object is a stream of its + // own. A legacy encrypted object without that marker is one + // continuous stream that the erasure writer split into storage + // fragments, so no fragment has a plaintext length to report and + // each keeps its stored size, as ObjectInfo.DecryptedSize and + // DecryptBlocksRequestR also treat it. + hasEncryptedParts := isEncrypted && + (crypto.IsMultiPart(objInfo.UserDefined) || len(objInfo.Parts) == 1) + OA.ObjectParts = new(objectAttributesParts) OA.ObjectParts.PartNumberMarker = opts.PartNumberMarker @@ -676,9 +718,38 @@ func (api objectAPIHandlers) getObjectAttributesHandler(ctx context.Context, obj } if len(OA.ObjectParts.Parts) == opts.MaxParts { + // This page is full and at least one more part + // is still pending, so the listing is truncated. + OA.ObjectParts.IsTruncated = true break } + partSize := objInfo.Parts[i].Size + switch { + case isCompressed: + // ActualSize is recorded by the same code that compresses, + // so it is always present for a compressed part. + if objInfo.Parts[i].ActualSize >= 0 { + partSize = objInfo.Parts[i].ActualSize + } + case hasEncryptedParts: + // ActualSize cannot be trusted for encrypted parts: a + // replicated SSE-C part records the ciphertext length, and + // parts written before actualSize existed record 0. Derive + // the plaintext length from the ciphertext instead, exactly + // as ObjectInfo.DecryptedSize does. A part whose stored + // length is not a valid encrypted stream has no logical + // length to report, and DecryptObjectInfo above only + // validates the object as a whole in that case, because + // ObjectInfo.isMultipart gives up on the first bad part. + decrypted, err := sio.DecryptedSize(uint64(partSize)) + if err != nil { + writeErrorResponse(ctx, w, toAPIError(ctx, errObjectTampered), r.URL) + return + } + partSize = int64(decrypted) + } + OA.ObjectParts.NextPartNumberMarker = v.Number OA.ObjectParts.Parts = append(OA.ObjectParts.Parts, &objectAttributesPart{ ChecksumSHA1: objInfo.Parts[i].Checksums["SHA1"], @@ -687,13 +758,16 @@ func (api objectAPIHandlers) getObjectAttributesHandler(ctx context.Context, obj ChecksumCRC32C: objInfo.Parts[i].Checksums["CRC32C"], ChecksumCRC64NVME: objInfo.Parts[i].Checksums["CRC64NVME"], PartNumber: objInfo.Parts[i].Number, - Size: objInfo.Parts[i].Size, + Size: partSize, }) } } - if OA.ObjectParts.NextPartNumberMarker != partsLength { - OA.ObjectParts.IsTruncated = true + // Part numbers may be sparse, so they cannot be compared against + // the part count. NextPartNumberMarker only carries a continuation + // token for a truncated listing, as in ListObjectParts. + if !OA.ObjectParts.IsTruncated { + OA.ObjectParts.NextPartNumberMarker = 0 } } @@ -791,6 +865,18 @@ func (api objectAPIHandlers) headObjectHandler(ctx context.Context, objectAPI Ob writeErrorResponseHeadersOnly(w, errorCodes.ToAPIErr(s3Error)) return } + if hasReplicationMarkerHeader(r.Header) { + trusted := hasReplicationMarker(r.Header) && + replicationPermissionAllowed(ctx, r, bucket, object, policy.ReplicateObjectAction) + ctx, r = applyReplicationTrust(ctx, r, trusted, trusted) + if trusted { + opts, err = getOpts(ctx, r, bucket, object) + if err != nil { + writeErrorResponseHeadersOnly(w, toAPIError(ctx, err)) + return + } + } + } // Get request range. var rs *HTTPRangeSpec @@ -902,8 +988,8 @@ func (api objectAPIHandlers) headObjectHandler(ctx context.Context, objectAPI Ob } // filter object lock metadata if permission does not permit - getRetPerms := checkRequestAuthType(ctx, r, policy.GetObjectRetentionAction, bucket, object) - legalHoldPerms := checkRequestAuthType(ctx, r, policy.GetObjectLegalHoldAction, bucket, object) + getRetPerms := authorizeRequest(ctx, r, policy.GetObjectRetentionAction) + legalHoldPerms := authorizeRequest(ctx, r, policy.GetObjectLegalHoldAction) // filter object lock metadata if permission does not permit objInfo.UserDefined = objectlock.FilterObjectLockMetadata(objInfo.UserDefined, getRetPerms != ErrNone, legalHoldPerms != ErrNone) @@ -929,10 +1015,12 @@ func (api objectAPIHandlers) headObjectHandler(ctx context.Context, objectAPI Ob w.Header().Set(xhttp.AmzServerSideEncryptionKmsContext, kmsCtx) } case crypto.SSEC: - // Validate the SSE-C Key set in the header. - if _, err = crypto.SSEC.UnsealObjectKey(r.Header, objInfo.UserDefined, bucket, object); err != nil { - writeErrorResponseHeadersOnly(w, toAPIError(ctx, err)) - return + if !isReplicaTrusted(ctx) { + // Validate the SSE-C Key set in the header for ordinary reads. + if _, err = crypto.SSEC.UnsealObjectKey(r.Header, objInfo.UserDefined, bucket, object); err != nil { + writeErrorResponseHeadersOnly(w, toAPIError(ctx, err)) + return + } } w.Header().Set(xhttp.AmzServerSideEncryptionCustomerAlgorithm, r.Header.Get(xhttp.AmzServerSideEncryptionCustomerAlgorithm)) w.Header().Set(xhttp.AmzServerSideEncryptionCustomerKeyMD5, r.Header.Get(xhttp.AmzServerSideEncryptionCustomerKeyMD5)) @@ -1084,29 +1172,12 @@ func getCpObjMetadataFromHeader(ctx context.Context, r *http.Request, userMeta m return defaultMeta, nil } -func cloneRequestWithoutCopyReplicationHeaders(r *http.Request) *http.Request { - if r == nil { - return nil - } - - clone := new(http.Request) - *clone = *r - clone.Header = r.Header.Clone() - - for _, header := range []string{ - xhttp.MinIOSourceReplicationRequest, - xhttp.MinIOSourceETag, - xhttp.MinIOSourceMTime, - xhttp.MinIOSourceTaggingTimestamp, - xhttp.MinIOSourceObjectRetentionTimestamp, - xhttp.MinIOSourceObjectLegalHoldTimestamp, - xhttp.MinIOReplicationActualObjectSize, - ReplicationSsecChecksumHeader, - } { - clone.Header.Del(header) - } - - return clone +func copyDestinationSSEHeaders(h http.Header) http.Header { + dst := h.Clone() + dst.Del(xhttp.AmzServerSideEncryptionCopyCustomerAlgorithm) + dst.Del(xhttp.AmzServerSideEncryptionCopyCustomerKey) + dst.Del(xhttp.AmzServerSideEncryptionCopyCustomerKeyMD5) + return dst } // getRemoteInstanceTransport contains a roundtripper for external (not peers) servers @@ -1124,6 +1195,12 @@ func getRemoteInstanceTransport() http.RoundTripper { return nil } +// federatedInternalAppName is the minio-go application token that +// getRemoteInstanceClient attaches to every legacy federation proxy request. It +// is declared next to its only producer so that the literal keeps its historical +// file attribution in the rebrand compatibility baseline. +const federatedInternalAppName = "minio-federated" + // Returns a minio-go Client configured to access remote host described by destDNSRecord // Applicable only in a federated deployment var getRemoteInstanceClient = func(r *http.Request, host string) (*miniogo.Core, error) { @@ -1138,7 +1215,7 @@ var getRemoteInstanceClient = func(r *http.Request, host string) (*miniogo.Core, if err != nil { return nil, err } - core.SetAppInfo("minio-federated", ReleaseTag) + core.SetAppInfo(federatedInternalAppName, ReleaseTag) return core, nil } @@ -1167,6 +1244,31 @@ func isRemoteCallRequired(ctx context.Context, bucket string, objAPI ObjectLayer return false } +// copyRewritesObjectData reports whether the object layer stores new object data +// for this copy instead of updating metadata in place or adding a +// self-referential version. It mirrors the metadata-only decision taken by +// erasureServerPools.CopyObject and erasureSets.CopyObject. CopyObjectHandler +// has to predict that decision because the compression metadata it records must +// describe whichever bytes are finally stored. metadataOnly already excludes +// legacy sources, which the object layer always rewrites. +func copyRewritesObjectData(metadataOnly bool, srcOpts, dstOpts ObjectOptions) bool { + if !metadataOnly { + return true + } + switch { + case dstOpts.VersionID != "" && srcOpts.VersionID == dstOpts.VersionID: + // In-place update of the addressed version. + return false + case !dstOpts.Versioned && srcOpts.VersionID == "": + // In-place update of an unversioned object. + return false + case dstOpts.Versioned && srcOpts.VersionID != dstOpts.VersionID: + // A new version referencing the existing data. + return false + } + return true +} + // CopyObjectHandler - Copy Object // ---------- // This implementation of the PUT operation adds an object to a bucket @@ -1255,28 +1357,23 @@ func (api objectAPIHandlers) CopyObjectHandler(w http.ResponseWriter, r *http.Re writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrInvalidStorageClass), r.URL) return } - allowReplicationMetadata := false - if r.Header.Get(xhttp.AmzBucketReplicationStatus) == replication.Replica.String() { - if s3Error := checkRequestAuthType(ctx, r, policy.ReplicateObjectAction, dstBucket, dstObject); s3Error != ErrNone { - writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL) - return - } - allowReplicationMetadata = true + trustedReplication, replicaTrusted, trustErr := evaluateReplicationTrust(ctx, r, dstBucket, dstObject, policy.ReplicateObjectAction) + if trustErr != ErrNone { + writeErrorResponse(ctx, w, errorCodes.ToAPIErr(trustErr), r.URL) + return } - trustedReplicationRequest := allowReplicationMetadata && r.Header.Get(xhttp.MinIOSourceReplicationRequest) == "true" - optsReq := r - if !trustedReplicationRequest { - optsReq = cloneRequestWithoutCopyReplicationHeaders(r) + if hasReplicationRequestHeaders(r.Header) { + ctx, r = applyReplicationTrust(ctx, r, trustedReplication, replicaTrusted) } + allowReplicationMetadata := replicaTrusted // Check if bucket encryption is enabled sseConfig, _ := globalBucketSSEConfigSys.Get(dstBucket) sseConfig.Apply(r.Header, sse.ApplyOptions{ AutoEncrypt: globalAutoEncryption, }) - var srcOpts, dstOpts ObjectOptions - srcOpts, err = copySrcOpts(ctx, optsReq, srcBucket, srcObject) + srcOpts, err = copySrcOpts(ctx, r, srcBucket, srcObject) if err != nil { writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL) return @@ -1288,14 +1385,14 @@ func (api objectAPIHandlers) CopyObjectHandler(w http.ResponseWriter, r *http.Re VersionID: srcOpts.VersionID, Versioned: srcOpts.Versioned, VersionSuspended: srcOpts.VersionSuspended, - ReplicationRequest: trustedReplicationRequest, + ReplicationRequest: replicaTrusted, } getSSE := encrypt.SSE(srcOpts.ServerSideEncryption) if getSSE != srcOpts.ServerSideEncryption { getOpts.ServerSideEncryption = getSSE } - dstOpts, err = copyDstOpts(ctx, optsReq, dstBucket, dstObject, nil) + dstOpts, err = copyDstOpts(ctx, r, dstBucket, dstObject, nil) if err != nil { writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL) return @@ -1305,7 +1402,7 @@ func (api objectAPIHandlers) CopyObjectHandler(w http.ResponseWriter, r *http.Re getObjectNInfo := objectAPI.GetObjectNInfo checkCopyPrecondFn := func(o ObjectInfo) bool { - if _, err := DecryptObjectInfo(&o, optsReq); err != nil { + if _, err := DecryptObjectInfo(&o, r); err != nil { writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL) return true } @@ -1359,6 +1456,15 @@ func (api objectAPIHandlers) CopyObjectHandler(w http.ResponseWriter, r *http.Re } // no changes in storage-class expected so its a metadataonly operation. var reader io.Reader = gr + sourceCompressMetadata := make(map[string]string, 2) + for _, key := range []string{ + ReservedMetadataPrefix + "compression", + ReservedMetadataPrefix + "actual-size", + } { + if value, ok := srcInfo.UserDefined[key]; ok { + sourceCompressMetadata[key] = value + } + } // Set the actual size to the compressed/decrypted size if encrypted. actualSize, err := srcInfo.GetActualSize() @@ -1388,15 +1494,7 @@ func (api objectAPIHandlers) CopyObjectHandler(w http.ResponseWriter, r *http.Re compressMetadata[ReservedMetadataPrefix+"actual-size"] = strconv.FormatInt(actualSize, 10) reader = etag.NewReader(ctx, reader, nil, nil) - wantEncryption := crypto.Requested(r.Header) - s2c, cb := newS2CompressReader(reader, actualSize, wantEncryption) - dstOpts.IndexCB = cb - defer s2c.Close() - reader = etag.Wrap(s2c, reader) - length = -1 } else { - delete(srcInfo.UserDefined, ReservedMetadataPrefix+"compression") - delete(srcInfo.UserDefined, ReservedMetadataPrefix+"actual-size") reader = gr } @@ -1416,7 +1514,7 @@ func (api objectAPIHandlers) CopyObjectHandler(w http.ResponseWriter, r *http.Re return } // Encryption parameters not present for this object. - if crypto.SSEC.IsEncrypted(srcInfo.UserDefined) && !crypto.SSECopy.IsRequested(r.Header) && !trustedReplicationRequest { + if crypto.SSEC.IsEncrypted(srcInfo.UserDefined) && !crypto.SSECopy.IsRequested(r.Header) && !replicaTrusted { writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrInvalidSSECustomerAlgorithm), r.URL) return } @@ -1450,12 +1548,39 @@ func (api objectAPIHandlers) CopyObjectHandler(w http.ResponseWriter, r *http.Re } } + // Name the source version explicitly so a metadata-only copy into a + // versioned bucket adds a self-referential version instead of rewriting the + // object data. A null source version cannot be referenced this way. + copySrcOpts := srcOpts + if dstOpts.Versioned && copySrcOpts.VersionID == "" { + copySrcOpts.VersionID = srcInfo.VersionID + } + + // A key rotation rewraps the object key held in metadata; it never + // re-encrypts the stored bytes. When the object layer stores new object + // data instead, the rotation has to go through the regular re-encrypting + // copy, or the destination ends up holding plaintext under metadata that + // claims the object is encrypted. + // + // A checksum algorithm the client asks for has to be computed over the object + // data, which an in-place rotation never reads, so leave the fast path and + // let the re-encrypting copy compute, store and report it. A replica-trusted + // request is not such a client: getOpts.ReplicationRequest leaves its source + // reader encrypted, so a rewrite would hash ciphertext, and a replica has to + // keep the checksum its source assigned. + // Without a requested destination checksum algorithm, an in-place SSE-C + // rotation preserves the stored checksum state, including absence; it does + // not add the default CRC64NVME. + canRotateKeyInPlace := !srcInfo.Legacy && + !copyRewritesObjectData(srcInfo.metadataOnly, copySrcOpts, dstOpts) && + (replicaTrusted || !hash.NewChecksumHeader(r.Header).IsSet()) + // 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 // - the object storage class is not changing // then execute a key rotation. - if cpSrcDstSame && (sseCopyC && sseC) && !chStorageClass { + if cpSrcDstSame && (sseCopyC && sseC) && !chStorageClass && canRotateKeyInPlace { oldKey, err = ParseSSECopyCustomerRequest(r.Header, srcInfo.UserDefined) if err != nil { writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL) @@ -1543,6 +1668,23 @@ func (api objectAPIHandlers) CopyObjectHandler(w http.ResponseWriter, r *http.Re } } + if isDstCompressed { + checksumReader := srcInfo.Reader + wantEncryption := crypto.Requested(r.Header) + s2c, cb := newS2CompressReader(checksumReader, actualSize, wantEncryption) + dstOpts.IndexCB = cb + defer s2c.Close() + reader = etag.Wrap(s2c, checksumReader) + srcInfo.Reader, err = hash.NewReader(ctx, reader, -1, "", "", actualSize) + if err != nil { + writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL) + return + } + // The storage reader consumes compressed data; checksums remain bound to plaintext. + pReader = NewPutObjReader(srcInfo.Reader) + pReader.setChecksumReader(checksumReader) + } + if isTargetEncrypted { var encReader io.Reader kind, _ := crypto.IsRequested(r.Header) @@ -1582,6 +1724,12 @@ func (api objectAPIHandlers) CopyObjectHandler(w http.ResponseWriter, r *http.Re srcInfo.PutObjReader = pReader + // Object Lock state as stored on disk, captured before the metadata + // directive rebuilds the map. A replica update is applied only when its + // source timestamp is newer than the stored one, and a stale update must + // leave the stored state in place instead of erasing it. + storedLock := storedObjectLockState(srcInfo.UserDefined) + srcInfo.UserDefined, err = getCpObjMetadataFromHeader(ctx, r, srcInfo.UserDefined, allowReplicationMetadata) if err != nil { writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL) @@ -1623,41 +1771,28 @@ func (api objectAPIHandlers) CopyObjectHandler(w http.ResponseWriter, r *http.Re getObjectInfo := objectAPI.GetObjectInfo // apply default bucket configuration/governance headers for dest side. - retentionMode, retentionDate, legalHold, s3Err := checkPutObjectLockAllowed(ctx, r, dstBucket, dstObject, getObjectInfo, retPerms, holdPerms) - if s3Err == ErrNone && retentionMode.Valid() { - lastretentionTimestamp := srcInfo.UserDefined[ReservedMetadataPrefixLower+ObjectLockRetentionTimestamp] - if dstOpts.ReplicationRequest { - srcTimestamp := dstOpts.ReplicationSourceRetentionTimestamp - if !srcTimestamp.IsZero() { - ondiskTimestamp, err := time.Parse(time.RFC3339Nano, lastretentionTimestamp) - // update retention metadata only if replica timestamp is newer than what's on disk - if err != nil || (err == nil && ondiskTimestamp.Before(srcTimestamp)) { - srcInfo.UserDefined[strings.ToLower(xhttp.AmzObjectLockMode)] = string(retentionMode) - srcInfo.UserDefined[strings.ToLower(xhttp.AmzObjectLockRetainUntilDate)] = amztime.ISO8601Format(retentionDate.UTC()) - srcInfo.UserDefined[ReservedMetadataPrefixLower+ObjectLockRetentionTimestamp] = srcTimestamp.UTC().Format(time.RFC3339Nano) - } - } - } else { - srcInfo.UserDefined[strings.ToLower(xhttp.AmzObjectLockMode)] = string(retentionMode) - srcInfo.UserDefined[strings.ToLower(xhttp.AmzObjectLockRetainUntilDate)] = amztime.ISO8601Format(retentionDate.UTC()) - srcInfo.UserDefined[ReservedMetadataPrefixLower+ObjectLockRetentionTimestamp] = UTCNow().Format(time.RFC3339Nano) - } - } + retentionMode, retentionDate, legalHold, s3Err := checkPutObjectLockAllowed(ctx, r, dstBucket, dstObject, getObjectInfo, retPerms, holdPerms, replicaTrusted) + if s3Err == ErrNone { + applyReplicatedObjectLock(srcInfo.UserDefined, storedLock, replicaTrusted, + retentionMode, retentionDate, legalHold, + dstOpts.ReplicationSourceRetentionTimestamp, dstOpts.ReplicationSourceLegalholdTimestamp) - if s3Err == ErrNone && legalHold.Status.Valid() { - lastLegalHoldTimestamp := srcInfo.UserDefined[ReservedMetadataPrefixLower+ObjectLockLegalHoldTimestamp] - if dstOpts.ReplicationRequest { - srcTimestamp := dstOpts.ReplicationSourceLegalholdTimestamp - if !srcTimestamp.IsZero() { - ondiskTimestamp, err := time.Parse(time.RFC3339Nano, lastLegalHoldTimestamp) - // update legalhold metadata only if replica timestamp is newer than what's on disk - if err != nil || (err == nil && ondiskTimestamp.Before(srcTimestamp)) { - srcInfo.UserDefined[strings.ToLower(xhttp.AmzObjectLockLegalHold)] = string(legalHold.Status) - srcInfo.UserDefined[ReservedMetadataPrefixLower+ObjectLockRetentionTimestamp] = srcTimestamp.Format(time.RFC3339Nano) + if replicaTrusted { + // An SSE-C key rotation snapshots every stored reserved key into + // encMetadata above, before this decision exists, and the merge that + // preserves the encryption headers would put the stored ordering + // timestamps back over it. For a trusted replica the decision just + // made is authoritative, so let the snapshot agree with it. + for _, key := range []string{ + ReservedMetadataPrefixLower + ObjectLockRetentionTimestamp, + ReservedMetadataPrefixLower + ObjectLockLegalHoldTimestamp, + } { + if value, ok := srcInfo.UserDefined[key]; ok { + encMetadata[key] = value + } else { + delete(encMetadata, key) } } - } else { - srcInfo.UserDefined[strings.ToLower(xhttp.AmzObjectLockLegalHold)] = string(legalHold.Status) } } if s3Err != ErrNone { @@ -1678,8 +1813,21 @@ func (api objectAPIHandlers) CopyObjectHandler(w http.ResponseWriter, r *http.Re srcInfo.UserDefined[ReservedMetadataPrefixLower+ReplicationStatus] = dsc.PendingStatus() srcInfo.UserDefined[ReservedMetadataPrefixLower+ReplicationTimestamp] = UTCNow().Format(time.RFC3339Nano) } - // Store the preserved compression metadata. - maps.Copy(srcInfo.UserDefined, compressMetadata) + // srcInfo.metadataOnly is still cleared below for legacy sources and for + // server-side checksum recomputation; both of those rewrite the object data. + metadataOnly := srcInfo.metadataOnly && !srcInfo.Legacy && !dstOpts.WantServerSideChecksumType.IsSet() + + // Compression metadata must describe the bytes that are actually stored. + if copyRewritesObjectData(metadataOnly, copySrcOpts, dstOpts) { + if isDstCompressed { + maps.Copy(srcInfo.UserDefined, compressMetadata) + } else { + delete(srcInfo.UserDefined, ReservedMetadataPrefix+"compression") + delete(srcInfo.UserDefined, ReservedMetadataPrefix+"actual-size") + } + } else { + maps.Copy(srcInfo.UserDefined, sourceCompressMetadata) + } // We need to preserve the encryption headers set in EncryptRequest, // so we do not want to override them, copy them instead. @@ -1771,7 +1919,7 @@ func (api objectAPIHandlers) CopyObjectHandler(w http.ResponseWriter, r *http.Re // Copy source object to destination, if source and destination // object is same then only metadata is updated. - objInfo, err = copyObjectFn(ctx, srcBucket, srcObject, dstBucket, dstObject, srcInfo, srcOpts, dstOpts) + objInfo, err = copyObjectFn(ctx, srcBucket, srcObject, dstBucket, dstObject, srcInfo, copySrcOpts, dstOpts) if err != nil { writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL) return @@ -1780,14 +1928,16 @@ func (api objectAPIHandlers) CopyObjectHandler(w http.ResponseWriter, r *http.Re origETag := objInfo.ETag objInfo.ETag = getDecryptedETag(r.Header, objInfo, false) - response := generateCopyObjectResponse(objInfo.ETag, objInfo.ModTime) + dstHeaders := copyDestinationSSEHeaders(r.Header) + checksums, _ := objInfo.decryptChecksums(0, dstHeaders) + response := generateCopyObjectResponse(objInfo, checksums) encodedSuccessResponse := encodeResponse(response) if dsc := mustReplicate(ctx, dstBucket, dstObject, objInfo.getMustReplicateOptions(replication.ObjectReplicationType, dstOpts)); dsc.ReplicateAny() { scheduleReplication(ctx, objInfo, objectAPI, dsc, replication.ObjectReplicationType) } - setPutObjHeaders(w, objInfo, false, r.Header) + setPutObjHeadersWithChecksum(w, objInfo, false, checksums) // We must not use the http.Header().Set method here because some (broken) // clients expect the x-amz-copy-source-version-id header key to be literally // "x-amz-copy-source-version-id"- not in canonicalized form, preserve it. @@ -1958,6 +2108,11 @@ func (api objectAPIHandlers) PutObjectHandler(w http.ResponseWriter, r *http.Req } } + 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 { // requests to just validate replication settings and permissions are not allowed to write data writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrReplicationPermissionCheckError), r.URL) @@ -1968,25 +2123,34 @@ func (api objectAPIHandlers) PutObjectHandler(w http.ResponseWriter, r *http.Req writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL) return } - if r.Header.Get(xhttp.AmzBucketReplicationStatus) == replication.Replica.String() { - if s3Err = isPutActionAllowed(ctx, getRequestAuthType(r), bucket, object, r, policy.ReplicateObjectAction); s3Err != ErrNone { - writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Err), r.URL) - return - } + if hasReplicationRequestHeaders(r.Header) { + ctx, r = applyReplicationTrust(ctx, r, trustedReplication, replicaTrusted) + } + if replicaTrusted { if err = extractReplicationMetadataFromMime(ctx, textproto.MIMEHeader(r.Header), metadata); err != nil { writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL) return } + metadata[xhttp.AmzBucketReplicationStatus] = replication.Replica.String() metadata[ReservedMetadataPrefixLower+ReplicaStatus] = replication.Replica.String() metadata[ReservedMetadataPrefixLower+ReplicaTimestamp] = UTCNow().Format(time.RFC3339Nano) defer globalReplicationStats.Load().UpdateReplicaStat(bucket, size) + } else { + delete(metadata, xhttp.AmzBucketReplicationStatus) } - // Check if bucket encryption is enabled - sseConfig, _ := globalBucketSSEConfigSys.Get(bucket) - sseConfig.Apply(r.Header, sse.ApplyOptions{ - AutoEncrypt: globalAutoEncryption, - }) + // A validated raw SSE-C replica carries the source ciphertext and the + // source seal. Its bytes must be stored verbatim: default encryption would + // overwrite the SSE-C IV and compression would invalidate the ciphertext. + rawSSECReplica := isRawSSECReplica(r.Header, replicaTrusted) + + if !rawSSECReplica { + // Check if bucket encryption is enabled + sseConfig, _ := globalBucketSSEConfigSys.Get(bucket) + sseConfig.Apply(r.Header, sse.ApplyOptions{ + AutoEncrypt: globalAutoEncryption, + }) + } var reader io.Reader reader = rd @@ -2000,7 +2164,7 @@ func (api objectAPIHandlers) PutObjectHandler(w http.ResponseWriter, r *http.Req actualSize := size var idxCb func() []byte - if isCompressible(r.Header, object) && size > minCompressibleSize { + if !rawSSECReplica && isCompressible(r.Header, object) && size > minCompressibleSize { // Storing the compression metadata. metadata[ReservedMetadataPrefix+"compression"] = compressionAlgorithmV2 metadata[ReservedMetadataPrefix+"actual-size"] = strconv.FormatInt(size, 10) @@ -2065,9 +2229,21 @@ func (api objectAPIHandlers) PutObjectHandler(w http.ResponseWriter, r *http.Req r.Header.Get(xhttp.IfMatch) != "" || r.Header.Get(xhttp.IfNoneMatch) != "" { opts.CheckPrecondFn = func(oi ObjectInfo) bool { - if _, err := DecryptObjectInfo(&oi, r); err != nil { - writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL) - return true + // A pure raw SSE-C replica overwrite (no public precondition) fully + // replaces the stored object, so the destination must not first + // require the stored object to decrypt: a replica an older destination + // bug left as compress(ciphertext) or double-encrypted has an invalid + // decrypted length, and requiring it here blocks the retransmission + // that repairs it. The predicate is the incoming request's restored + // SSE-C metadata, the same one checkPreconditionsPUT uses to exempt the + // version/ETag duplicate. A conditional request (If-Match/If-None-Match) + // still needs the decrypted, client-visible ETag, so it keeps the check. + ssecReplica := isReplicaTrusted(ctx) && crypto.SSEC.IsEncrypted(opts.UserDefined) + if !ssecReplica || r.Header.Get(xhttp.IfMatch) != "" || r.Header.Get(xhttp.IfNoneMatch) != "" { + if _, err := DecryptObjectInfo(&oi, r); err != nil { + writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL) + return true + } } return checkPreconditionsPUT(ctx, w, r, oi, opts) } @@ -2078,13 +2254,32 @@ func (api objectAPIHandlers) PutObjectHandler(w http.ResponseWriter, r *http.Req getObjectInfo := objectAPI.GetObjectInfo - retentionMode, retentionDate, legalHold, s3Err := checkPutObjectLockAllowed(ctx, r, bucket, object, getObjectInfo, retPerms, holdPerms) - if s3Err == ErrNone && retentionMode.Valid() { - metadata[strings.ToLower(xhttp.AmzObjectLockMode)] = string(retentionMode) - metadata[strings.ToLower(xhttp.AmzObjectLockRetainUntilDate)] = amztime.ISO8601Format(retentionDate.UTC()) - } - if s3Err == ErrNone && legalHold.Status.Valid() { - metadata[strings.ToLower(xhttp.AmzObjectLockLegalHold)] = string(legalHold.Status) + retentionMode, retentionDate, legalHold, s3Err := checkPutObjectLockAllowed(ctx, r, bucket, object, getObjectInfo, retPerms, holdPerms, isReplicaTrusted(ctx)) + if s3Err == ErrNone { + // A trusted replica write addressing a specific version can be a full + // retransmit over an existing version whose lock state is newer than the + // source snapshot (issue #120). Order the incoming update against what is + // stored so a stale value cannot overwrite it; a non-replica write (and a + // marker-only peer write) has no stored state to order against and takes + // the helper's ordinary-write branch. + var storedLock objectLockState + if isReplicaTrusted(ctx) && opts.VersionID != "" { + var lerr error + if storedLock, lerr = replicaStoredLock(ctx, getObjectInfo, bucket, object, opts.VersionID); lerr != nil { + writeErrorResponse(ctx, w, toAPIError(ctx, lerr), r.URL) + return + } + } + applyReplicatedObjectLock(metadata, storedLock, isReplicaTrusted(ctx), + retentionMode, retentionDate, legalHold, + opts.ReplicationSourceRetentionTimestamp, opts.ReplicationSourceLegalholdTimestamp) + // The decision above orders against the version as read here; let the + // object layer re-run it against the version read under the write lock + // that guards the replacement, so a newer hold or retention committed in + // between is not rolled back (issue #120). Scoped to the SSE-C replica + // retransmit this issue enables, keyed on the incoming write's restored + // SSE-C seal, the same predicate as the duplicate-version exemption. + opts.ReplicaLockReconcile = isReplicaTrusted(ctx) && opts.VersionID != "" && crypto.SSEC.IsEncrypted(metadata) } if s3Err != ErrNone { writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Err), r.URL) @@ -2095,7 +2290,10 @@ func (api objectAPIHandlers) PutObjectHandler(w http.ResponseWriter, r *http.Req metadata[ReservedMetadataPrefixLower+ReplicationStatus] = dsc.PendingStatus() } var objectEncryptionKey crypto.ObjectKey - if crypto.Requested(r.Header) { + // rawSSECReplica also gates the branch itself, not just the default-SSE + // header that would normally reach it: a trusted peer that sends an + // explicit public SSE header alongside a source seal must not re-encrypt. + if !rawSSECReplica && crypto.Requested(r.Header) { if crypto.SSECopy.IsRequested(r.Header) { writeErrorResponse(ctx, w, toAPIError(ctx, errInvalidEncryptionParameters), r.URL) return @@ -2362,7 +2560,6 @@ func (api objectAPIHandlers) PutObjectExtractHandler(w http.ResponseWriter, r *h sha256hex = getContentSha256Cksum(r, serviceS3) } } - hreader, err := hash.NewReader(ctx, reader, size, md5hex, sha256hex, size) if err != nil { writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL) @@ -2383,14 +2580,26 @@ func (api objectAPIHandlers) PutObjectExtractHandler(w http.ResponseWriter, r *h sseConfig.Apply(r.Header, sse.ApplyOptions{ AutoEncrypt: globalAutoEncryption, }) + entryRequestBase := r.Clone(ctx) + // The streaming reader fills r.Trailer while untar writes small entries in + // parallel. Entry authorization never consumes trailers, so keep them out + // of the immutable request template cloned by those goroutines. Snapshot + // after applying bucket defaults so extracted objects retain encryption. + entryRequestBase.Trailer = nil + rawReplica := hasReplicaStatus(r.Header) + markerExact := hasReplicationMarker(r.Header) + trustedRequestCtx := withReplicationTrust(ctx, true, rawReplica) + trustedRequest := entryRequestBase.WithContext(trustedRequestCtx) + cleanRequestCtx := withReplicationTrust(ctx, false, false) + cleanRequest := cloneRequestWithoutReplicationHeaders(cleanRequestCtx, entryRequestBase) + trustedReqParams := extractReqParams(trustedRequest) + cleanReqParams := extractReqParams(cleanRequest) retPerms := isPutActionAllowed(ctx, getRequestAuthType(r), bucket, object, r, policy.PutObjectRetentionAction) holdPerms := isPutActionAllowed(ctx, getRequestAuthType(r), bucket, object, r, policy.PutObjectLegalHoldAction) getObjectInfo := objectAPI.GetObjectInfo - // These are static for all objects extracted. - reqParams := extractReqParams(r) respElements := map[string]string{ "requestId": w.Header().Get(xhttp.AmzRequestID), "nodeId": w.Header().Get(xhttp.AmzRequestHostID), @@ -2398,12 +2607,46 @@ func (api objectAPIHandlers) PutObjectExtractHandler(w http.ResponseWriter, r *h if sc == "" { sc = storageclass.STANDARD } + reqInfo := logger.GetReqInfo(ctx) + if reqInfo == nil { + writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrAccessDenied), r.URL) + return + } + reqInfo.RLock() + tarCred := reqInfo.Cred + tarOwner := reqInfo.Owner + reqInfo.RUnlock() + var tarS3Err atomic.Int32 + setTarS3Err := func(code APIErrorCode) { + tarS3Err.CompareAndSwap(int32(ErrNone), int32(code)) + } + ignoreEntryErrors := opts.ignoreErrs putObjectTar := func(reader io.Reader, info os.FileInfo, object string) error { size := info.Size() - if s3Err = isPutActionAllowed(ctx, getRequestAuthType(r), bucket, object, r, policy.PutObjectAction); s3Err != ErrNone { - writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Err), r.URL) - return errors.New(errorCodes.ToAPIErr(s3Err).Code) + entryAuthReq := entryRequestBase.Clone(ctx) + entryS3Err := isPutActionAllowedWithCred(bucket, object, entryAuthReq, policy.PutObjectAction, nil, tarCred, tarOwner) + if entryS3Err != ErrNone { + setTarS3Err(entryS3Err) + return errors.New(errorCodes.ToAPIErr(entryS3Err).Code) + } + replicationPermitted := false + if tarCred.AccessKey != "" && (rawReplica || markerExact) { + replicationPermitted = isPutActionAllowedWithCred(bucket, object, entryAuthReq, policy.ReplicateObjectAction, nil, tarCred, tarOwner) == ErrNone + } + if rawReplica && !replicationPermitted { + setTarS3Err(ErrAccessDenied) + return errors.New(errorCodes.ToAPIErr(ErrAccessDenied).Code) + } + entryTrusted := markerExact && replicationPermitted + replicaTrusted := entryTrusted && rawReplica + entryCtx := cleanRequestCtx + entryReq := cloneRequestWithoutReplicationHeaders(cleanRequestCtx, entryAuthReq) + reqParams := cleanReqParams + if entryTrusted { + entryCtx = trustedRequestCtx + entryReq = entryAuthReq.WithContext(trustedRequestCtx) + reqParams = trustedReqParams } metadata := map[string]string{ xhttp.AmzStorageClass: sc, // save same storage-class as incoming stream. @@ -2411,7 +2654,7 @@ func (api objectAPIHandlers) PutObjectExtractHandler(w http.ResponseWriter, r *h actualSize := size var idxCb func() []byte - if isCompressible(r.Header, object) && size > minCompressibleSize { + if isCompressible(entryReq.Header, object) && size > minCompressibleSize { // Storing the compression metadata. metadata[ReservedMetadataPrefix+"compression"] = compressionAlgorithmV2 metadata[ReservedMetadataPrefix+"actual-size"] = strconv.FormatInt(size, 10) @@ -2422,7 +2665,7 @@ func (api objectAPIHandlers) PutObjectExtractHandler(w http.ResponseWriter, r *h } // Set compression metrics. - wantEncryption := crypto.Requested(r.Header) + wantEncryption := crypto.Requested(entryReq.Header) s2c, cb := newS2CompressReader(actualReader, actualSize, wantEncryption) defer s2c.Close() idxCb = cb @@ -2438,15 +2681,11 @@ func (api objectAPIHandlers) PutObjectExtractHandler(w http.ResponseWriter, r *h rawReader := hashReader pReader := NewPutObjReader(rawReader) - allowReplicationMetadata := false - if r.Header.Get(xhttp.AmzBucketReplicationStatus) == replication.Replica.String() { - if s3Err = isPutActionAllowed(ctx, getRequestAuthType(r), bucket, object, r, policy.ReplicateObjectAction); s3Err != ErrNone { - return errors.New(errorCodes.ToAPIErr(s3Err).Code) - } - allowReplicationMetadata = true - if err = extractReplicationMetadataFromMime(ctx, textproto.MIMEHeader(r.Header), metadata); err != nil { + if replicaTrusted { + if err := extractReplicationMetadataFromMime(entryCtx, textproto.MIMEHeader(entryReq.Header), metadata); err != nil { return err } + metadata[xhttp.AmzBucketReplicationStatus] = replication.Replica.String() metadata[ReservedMetadataPrefixLower+ReplicaStatus] = replication.Replica.String() metadata[ReservedMetadataPrefixLower+ReplicaTimestamp] = UTCNow().Format(time.RFC3339Nano) } @@ -2468,22 +2707,25 @@ func (api objectAPIHandlers) PutObjectExtractHandler(w http.ResponseWriter, r *h hdrs.Set(k, v) } } - m, err := extractMetadata(ctx, textproto.MIMEHeader(hdrs)) + if !entryTrusted { + stripReplicationRequestHeaders(hdrs) + } + m, err := extractMetadata(entryCtx, textproto.MIMEHeader(hdrs)) if err != nil { return err } - if allowReplicationMetadata { - if err = extractReplicationMetadataFromMime(ctx, textproto.MIMEHeader(hdrs), m); err != nil { + if replicaTrusted { + if err = extractReplicationMetadataFromMime(entryCtx, textproto.MIMEHeader(hdrs), m); err != nil { return err } } maps.Copy(metadata, m) } else { - versionID = r.Form.Get(xhttp.VersionID) - hdrs = r.Header + versionID = entryReq.Form.Get(xhttp.VersionID) + hdrs = entryReq.Header } - opts, err := putOpts(ctx, bucket, object, versionID, hdrs, metadata) + opts, err := putOpts(entryCtx, bucket, object, versionID, hdrs, metadata, entryTrusted) if err != nil { return err } @@ -2494,7 +2736,7 @@ func (api objectAPIHandlers) PutObjectExtractHandler(w http.ResponseWriter, r *h } opts.IndexCB = idxCb - retentionMode, retentionDate, legalHold, s3err := checkPutObjectLockAllowed(ctx, r, bucket, object, getObjectInfo, retPerms, holdPerms) + retentionMode, retentionDate, legalHold, s3err := checkPutObjectLockAllowed(entryCtx, entryReq, bucket, object, getObjectInfo, retPerms, holdPerms, replicaTrusted) if s3err == ErrNone && retentionMode.Valid() { metadata[strings.ToLower(xhttp.AmzObjectLockMode)] = string(retentionMode) metadata[strings.ToLower(xhttp.AmzObjectLockRetainUntilDate)] = amztime.ISO8601Format(retentionDate.UTC()) @@ -2505,7 +2747,9 @@ func (api objectAPIHandlers) PutObjectExtractHandler(w http.ResponseWriter, r *h } if s3err != ErrNone { - s3Err = s3err + if !ignoreEntryErrors { + setTarS3Err(s3err) + } return ObjectLocked{} } @@ -2515,12 +2759,12 @@ func (api objectAPIHandlers) PutObjectExtractHandler(w http.ResponseWriter, r *h } var objectEncryptionKey crypto.ObjectKey - if crypto.Requested(r.Header) { - if crypto.SSECopy.IsRequested(r.Header) { + if crypto.Requested(entryReq.Header) { + if crypto.SSECopy.IsRequested(entryReq.Header) { return errInvalidEncryptionParameters } - reader, objectEncryptionKey, err = EncryptRequest(hashReader, r, bucket, object, metadata) + reader, objectEncryptionKey, err = EncryptRequest(hashReader, entryReq, bucket, object, metadata) if err != nil { return err } @@ -2565,7 +2809,7 @@ func (api objectAPIHandlers) PutObjectExtractHandler(w http.ResponseWriter, r *h } origETag := objInfo.ETag - objInfo.ETag = getDecryptedETag(r.Header, objInfo, false) + objInfo.ETag = getDecryptedETag(entryReq.Header, objInfo, false) if dsc := mustReplicate(ctx, bucket, object, getMustReplicateOptions(metadata, "", "", replication.ObjectReplicationType, opts)); dsc.ReplicateAny() { scheduleReplication(ctx, objInfo, objectAPI, dsc, replication.ObjectReplicationType) @@ -2578,8 +2822,8 @@ func (api objectAPIHandlers) PutObjectExtractHandler(w http.ResponseWriter, r *h Object: objInfo, ReqParams: reqParams, RespElements: respElements, - UserAgent: r.UserAgent(), - Host: handlers.GetSourceIP(r), + UserAgent: entryReq.UserAgent(), + Host: handlers.GetSourceIP(entryReq), } sendEvent(evt) @@ -2594,7 +2838,14 @@ func (api objectAPIHandlers) PutObjectExtractHandler(w http.ResponseWriter, r *h return nil } - if err = untar(ctx, hreader, putObjectTar, opts); err != nil { + err = untar(ctx, hreader, putObjectTar, opts) + if code := APIErrorCode(tarS3Err.Load()); code != ErrNone { + s3Err = code + if err == nil { + err = errors.New(errorCodes.ToAPIErr(code).Code) + } + } + if err != nil { apiErr := errorCodes.ToAPIErr(s3Err) // If not set, convert or use BadRequest if s3Err == ErrNone { @@ -2639,7 +2890,29 @@ func (api objectAPIHandlers) DeleteObjectHandler(w http.ResponseWriter, r *http. return } - if s3Error := checkRequestAuthType(ctx, r, policy.DeleteObjectAction, bucket, object); s3Error != ErrNone { + reqInfo := logger.GetReqInfo(ctx) + if reqInfo == nil { + writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrAccessDenied), r.URL) + return + } + reqInfo.BucketName = bucket + reqInfo.ObjectName = object + if s3Error := authenticateRequest(ctx, r, policy.DeleteObjectAction); s3Error != ErrNone { + writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL) + return + } + trustedReplication, replica, trustErr := evaluateReplicationTrust(ctx, r, bucket, object, policy.ReplicateDeleteAction) + if trustErr != ErrNone { + writeErrorResponse(ctx, w, errorCodes.ToAPIErr(trustErr), r.URL) + return + } + var s3Error APIErrorCode + if trustedReplication { + s3Error = authorizeReplicationDelete(ctx, r) + } else { + s3Error = authorizeRequest(ctx, r, deleteObjectAction(reqInfo.VersionID)) + } + if s3Error != ErrNone { writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL) return } @@ -2648,13 +2921,8 @@ func (api objectAPIHandlers) DeleteObjectHandler(w http.ResponseWriter, r *http. writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrReplicationPermissionCheckError), r.URL) return } - - replica := r.Header.Get(xhttp.AmzBucketReplicationStatus) == replication.Replica.String() - if replica { - if s3Error := checkRequestAuthType(ctx, r, policy.ReplicateDeleteAction, bucket, object); s3Error != ErrNone { - writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL) - return - } + if hasReplicationRequestHeaders(r.Header) { + ctx, r = applyReplicationTrust(ctx, r, trustedReplication, replica) } if globalDNSConfig != nil { @@ -2671,6 +2939,20 @@ func (api objectAPIHandlers) DeleteObjectHandler(w http.ResponseWriter, r *http. return } + // If-Match conditional delete (see AWS S3 conditional deletes). Delete the + // object only if its current ETag matches the client-supplied value, + // otherwise the request is refused with 412 Precondition Failed and the + // object is left intact. The precondition is evaluated in + // erasureServerPools.DeleteObject, against the version that will actually be + // removed, while the delete lock is held, so the object cannot change + // between the ETag check and the delete. + if ifMatch := r.Header.Get(xhttp.IfMatch); ifMatch != "" { + opts.HasIfMatch = true + opts.CheckPrecondFn = func(oi ObjectInfo) bool { + return deleteIfMatchPreconditionFailed(r.Header, ifMatch, oi) + } + } + rcfg, _ := globalBucketObjectLockSys.Get(bucket) if rcfg.LockEnabled && opts.DeletePrefix { apiErr := toAPIError(ctx, errInvalidArgument) @@ -2736,6 +3018,13 @@ func (api objectAPIHandlers) DeleteObjectHandler(w http.ResponseWriter, r *http. return } if isErrObjectNotFound(err) || isErrVersionNotFound(err) { + if opts.HasIfMatch { + // A conditional (If-Match) delete cannot satisfy its + // precondition against a missing object, so surface the + // not-found error instead of the idempotent 204 response. + writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL) + return + } // Send an event when the object is not found objInfo.Name = object objInfo.VersionID = opts.VersionID diff --git a/cmd/object-handlers_tampered_test.go b/cmd/object-handlers_tampered_test.go new file mode 100644 index 000000000..87fc448b0 --- /dev/null +++ b/cmd/object-handlers_tampered_test.go @@ -0,0 +1,64 @@ +package cmd + +import ( + "context" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/minio/minio/internal/auth" + "github.com/minio/minio/internal/crypto" +) + +// tamperedObjectLayer returns a fixed error from the read paths so the handler +// response can be tested without reproducing the storage defect that produces +// an unreadable object. No object bytes are sent to the caller. +type tamperedObjectLayer struct { + ObjectLayer + err error +} + +func (o *tamperedObjectLayer) GetObjectNInfo(context.Context, string, string, *HTTPRangeSpec, http.Header, ObjectOptions) (*GetObjectReader, error) { + return nil, o.err +} + +func (o *tamperedObjectLayer) GetObjectInfo(context.Context, string, string, ObjectOptions) (ObjectInfo, error) { + return ObjectInfo{}, o.err +} + +// TestObjectTamperedGETHEADStatus asserts that an object the server cannot +// decode is reported with a 5xx status, not a success status. Returning +// http.StatusPartialContent here let SDKs accept the XML error document as +// object content. See pgsty/silo#110. +func TestObjectTamperedGETHEADStatus(t *testing.T) { + ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{t: t, objAPITest: func(obj ObjectLayer, instanceType, bucket string, router http.Handler, credentials auth.Credentials, t *testing.T) { + // An encrypted stream shorter than one complete encryption package is + // a real size-validation origin of errObjectTampered. + damaged := ObjectInfo{Size: 31, UserDefined: map[string]string{crypto.MetaAlgorithm: crypto.InsecureSealAlgorithm}} + _, err := damaged.DecryptedSize() + if err != errObjectTampered { + t.Fatalf("damaged-size error = %v, want errObjectTampered", err) + } + previous := newObjectLayerFn() + setObjectLayer(&tamperedObjectLayer{ObjectLayer: obj, err: err}) + defer setObjectLayer(previous) + for _, method := range []string{http.MethodGet, http.MethodHead} { + req, err := newTestSignedRequestV4(method, getGetObjectURL("", bucket, "damaged-object"), 0, nil, credentials.AccessKey, credentials.SecretKey, nil) + if err != nil { + t.Fatal(err) + } + rec := httptest.NewRecorder() + router.ServeHTTP(rec, req) + if method == http.MethodGet && !strings.Contains(rec.Body.String(), "XMinioObjectTampered") { + t.Fatalf("%s: GET did not reach the damaged-object response: %d %s", instanceType, rec.Code, rec.Body.String()) + } + if method == http.MethodHead && rec.Header().Get(xMinIOErrCodeHeader) != "XMinioObjectTampered" { + t.Fatalf("%s: HEAD did not reach the damaged-object response: %d %v", instanceType, rec.Code, rec.Header()) + } + if rec.Code != http.StatusInternalServerError { + t.Errorf("%s: %s of a damaged object returned HTTP %d, want %d", instanceType, method, rec.Code, http.StatusInternalServerError) + } + } + }}) +} diff --git a/cmd/object-handlers_test.go b/cmd/object-handlers_test.go index a182d9e6f..d3cc64fdf 100644 --- a/cmd/object-handlers_test.go +++ b/cmd/object-handlers_test.go @@ -43,6 +43,7 @@ import ( "github.com/dustin/go-humanize" "github.com/minio/minio/internal/auth" "github.com/minio/minio/internal/crypto" + minhash "github.com/minio/minio/internal/hash" "github.com/minio/minio/internal/hash/sha256" xhttp "github.com/minio/minio/internal/http" ioutilx "github.com/minio/minio/internal/ioutil" @@ -978,7 +979,10 @@ func testAPIGetObjectWithPartNumberHandler(obj ObjectLayer, instanceType, bucket t.Fatalf("Object: %s Object Index %d: Unexpected err: %v", object, oindex, err) } - rs := partNumberToRangeSpec(oinfo, partNumber) + rs, err := partNumberToRangeSpec(oinfo, partNumber) + if err != nil { + t.Fatalf("Object: %s Object Index %d: Unexpected err: %v", object, oindex, err) + } size, err := oinfo.GetActualSize() if err != nil { t.Fatalf("Object: %s Object Index %d: Unexpected err: %v", object, oindex, err) @@ -3386,6 +3390,62 @@ func testAPICompleteMultipartHandler(obj ObjectLayer, instanceType, bucketName s ExecObjectLayerAPINilTest(t, nilBucket, nilObject, instanceType, apiRouter, nilReq) } +// TestGenerateCompleteMultipartUploadResponseChecksumType verifies that +// ChecksumType is populated as FULL_OBJECT/COMPOSITE when the object carries +// a checksum, and omitted from the XML entirely when it doesn't. +func TestGenerateCompleteMultipartUploadResponseChecksumType(t *testing.T) { + bucket, key := "test-bucket", "test-object" + + testCases := []struct { + name string + checksum *minhash.Checksum + wantChecksumType string + }{ + { + name: "no checksum", + checksum: nil, + wantChecksumType: "", + }, + { + name: "full object checksum", + checksum: minhash.NewChecksumFromData(minhash.ChecksumCRC32, []byte("full-object-data")), + wantChecksumType: xhttp.AmzChecksumTypeFullObject, + }, + { + name: "composite multipart checksum", + checksum: func() *minhash.Checksum { + c := minhash.NewChecksumFromData(minhash.ChecksumCRC32C|minhash.ChecksumMultipart, []byte("combined-part-checksums")) + c.WantParts = 2 + return c + }(), + wantChecksumType: xhttp.AmzChecksumTypeComposite, + }, + } + + for _, tt := range testCases { + t.Run(tt.name, func(t *testing.T) { + oi := ObjectInfo{ETag: "d41d8cd98f00b204e9800998ecf8427e"} + if tt.checksum != nil { + oi.Checksum = tt.checksum.AppendTo(nil, nil) + } + + resp := generateCompleteMultipartUploadResponse(bucket, key, getGetObjectURL("", bucket, key), oi, nil) + if resp.ChecksumType != tt.wantChecksumType { + t.Fatalf("ChecksumType: got %q, want %q", resp.ChecksumType, tt.wantChecksumType) + } + + encoded, err := xml.Marshal(resp) + if err != nil { + t.Fatalf("failed to marshal response: %v", err) + } + gotTag := strings.Contains(string(encoded), "") + if wantTag := tt.wantChecksumType != ""; gotTag != wantTag { + t.Fatalf("ChecksumType tag presence: got %v, want %v (xml: %s)", gotTag, wantTag, encoded) + } + }) + } +} + // The UploadID from the response body is parsed and its existence is asserted with an attempt to ListParts using it. func TestAPIAbortMultipartHandler(t *testing.T) { defer DetectTestLeak(t)() diff --git a/cmd/object-lambda-handlers.go b/cmd/object-lambda-handlers.go index 1ced5165d..35e06c859 100644 --- a/cmd/object-lambda-handlers.go +++ b/cmd/object-lambda-handlers.go @@ -32,7 +32,7 @@ import ( miniogo "github.com/minio/minio-go/v7" "github.com/minio/minio-go/v7/pkg/credentials" "github.com/minio/mux" - "github.com/minio/pkg/v3/policy" + "github.com/pgsty/silo-pkg/v3/policy" "github.com/minio/minio/internal/auth" levent "github.com/minio/minio/internal/config/lambda/event" diff --git a/cmd/object-multipart-federation-checksum_test.go b/cmd/object-multipart-federation-checksum_test.go new file mode 100644 index 000000000..85269c7ee --- /dev/null +++ b/cmd/object-multipart-federation-checksum_test.go @@ -0,0 +1,367 @@ +// Copyright (c) 2015-2025 MinIO, Inc. +// Copyright (c) 2025-2026 PGSTY +// +// This file is part of MinIO Object Storage stack +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package cmd + +import ( + "bytes" + "context" + "crypto/md5" + "encoding/hex" + "encoding/json" + "encoding/xml" + "net/http" + "net/http/httptest" + "strings" + "sync" + "testing" + + miniogo "github.com/minio/minio-go/v7" + miniocredentials "github.com/minio/minio-go/v7/pkg/credentials" + "github.com/minio/minio-go/v7/pkg/set" + "github.com/minio/minio/internal/auth" + "github.com/minio/minio/internal/config/dns" + "github.com/minio/minio/internal/hash" + xhttp "github.com/minio/minio/internal/http" +) + +const federatedTestUserAgent = "MinIO (linux; amd64) minio-go/v7.3.1 minio-federated/RELEASE.TEST" + +func TestAPIFederatedUploadPartChecksumResponse(t *testing.T) { + defer DetectTestLeak(t)() + ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{ + t: t, + objAPITest: testAPIFederatedUploadPartChecksumResponse, + endpoints: []string{"PutObjectPart", "NewMultipart"}, + }) +} + +func testAPIFederatedUploadPartChecksumResponse(_ ObjectLayer, instanceType, bucketName string, + apiRouter http.Handler, credentials auth.Credentials, t *testing.T, +) { + algorithms := []struct { + name string + typ hash.ChecksumType + checksumType string + }{ + {name: "crc32-full-object", typ: hash.ChecksumCRC32, checksumType: xhttp.AmzChecksumTypeFullObject}, + {name: "sha256-composite", typ: hash.ChecksumSHA256, checksumType: xhttp.AmzChecksumTypeComposite}, + } + userAgents := []struct { + name string + ua string + want bool + }{ + {name: "absent"}, + {name: "ordinary-sdk", ua: "aws-sdk-go/1.55.5"}, + {name: "federation", ua: federatedTestUserAgent, want: true}, + {name: "lookalike-prefix", ua: "evil-minio-federated/RELEASE.TEST"}, + {name: "lookalike-suffix", ua: "minio-federated-extra/RELEASE.TEST"}, + {name: "missing-version", ua: "minio-federated"}, + {name: "empty-version", ua: "minio-federated/"}, + } + data := []byte("federated upload part checksum response") + + for _, algorithm := range algorithms { + for _, userAgent := range userAgents { + t.Run(algorithm.name+"/"+userAgent.name, func(t *testing.T) { + object := "federation/response/" + algorithm.name + "/" + userAgent.name + uploadID := newMultipartUploadHTTP(t, apiRouter, credentials, bucketName, object, + algorithm.typ.String(), algorithm.checksumType) + headers := map[string]string{} + if userAgent.ua != "" { + headers["User-Agent"] = userAgent.ua + } + _, rec := uploadPartHTTP(t, apiRouter, credentials, + bucketName, object, uploadID, 1, data, headers) + + got := rec.Header().Get(algorithm.typ.Key()) + if userAgent.want { + if want := mustChecksum(t, algorithm.typ, data); got != want { + t.Fatalf("%s: checksum %q, want %q", instanceType, got, want) + } + } else if got != "" { + t.Fatalf("%s: ordinary UploadPart exposed server checksum %q", instanceType, got) + } + if got := rec.Header().Get(xhttp.AmzChecksumType); got != "" { + t.Fatalf("%s: UploadPart returned checksum type %q", instanceType, got) + } + }) + } + } +} + +func TestAPIFederatedUploadPartChecksumMinIOGoWire(t *testing.T) { + defer DetectTestLeak(t)() + ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{ + t: t, + objAPITest: testAPIFederatedUploadPartChecksumMinIOGoWire, + endpoints: []string{"PutObjectPart", "NewMultipart"}, + }) +} + +func testAPIFederatedUploadPartChecksumMinIOGoWire(_ ObjectLayer, instanceType, bucketName string, + apiRouter http.Handler, credentials auth.Credentials, t *testing.T, +) { + server := httptest.NewServer(apiRouter) + defer server.Close() + + core, err := miniogo.NewCore(server.Listener.Addr().String(), &miniogo.Options{ + Creds: miniocredentials.NewStaticV4(credentials.AccessKey, credentials.SecretKey, ""), + Secure: false, + Region: globalMinioDefaultRegion, + BucketLookup: miniogo.BucketLookupPath, + }) + if err != nil { + t.Fatalf("%s: create minio-go Core: %v", instanceType, err) + } + core.SetAppInfo("minio-federated", ReleaseTag) + + object := "federation/minio-go-wire" + uploadID := newMultipartUploadHTTP(t, apiRouter, credentials, bucketName, object, + hash.ChecksumCRC32.String(), xhttp.AmzChecksumTypeFullObject) + data := []byte("minio-go must parse the remote computed checksum") + part, err := core.PutObjectPart(t.Context(), bucketName, object, uploadID, 1, + bytes.NewReader(data), int64(len(data)), miniogo.PutObjectPartOptions{}) + if err != nil { + t.Fatalf("%s: minio-go PutObjectPart: %v", instanceType, err) + } + if want := mustChecksum(t, hash.ChecksumCRC32, data); part.ChecksumCRC32 != want { + t.Fatalf("%s: minio-go checksum %q, want %q", instanceType, part.ChecksumCRC32, want) + } + if part.ETag == "" { + t.Fatalf("%s: minio-go returned an empty ETag", instanceType) + } +} + +func TestAPIFederatedUploadPartChecksumConcurrentOverwrite(t *testing.T) { + defer DetectTestLeak(t)() + ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{ + t: t, + objAPITest: testAPIFederatedUploadPartChecksumConcurrentOverwrite, + endpoints: []string{"PutObjectPart", "NewMultipart"}, + }) +} + +func testAPIFederatedUploadPartChecksumConcurrentOverwrite(_ ObjectLayer, instanceType, bucketName string, + apiRouter http.Handler, credentials auth.Credentials, t *testing.T, +) { + object := "federation/concurrent-overwrite" + uploadID := newMultipartUploadHTTP(t, apiRouter, credentials, bucketName, object, + hash.ChecksumSHA256.String(), xhttp.AmzChecksumTypeComposite) + data := [][]byte{ + bytes.Repeat([]byte("first-writer-"), 4096), + bytes.Repeat([]byte("second-writer-"), 4096), + } + reqs := make([]*http.Request, len(data)) + recorders := make([]*httptest.ResponseRecorder, len(data)) + for i := range data { + req, err := newTestSignedRequestV4(http.MethodPut, + getPutObjectPartURL("", bucketName, object, uploadID, "1"), + int64(len(data[i])), bytes.NewReader(data[i]), credentials.AccessKey, credentials.SecretKey, + map[string]string{"User-Agent": federatedTestUserAgent}) + if err != nil { + t.Fatalf("%s: build concurrent request %d: %v", instanceType, i, err) + } + reqs[i] = req + recorders[i] = httptest.NewRecorder() + } + + start := make(chan struct{}) + var wg sync.WaitGroup + for i := range reqs { + wg.Add(1) + go func() { + defer wg.Done() + <-start + apiRouter.ServeHTTP(recorders[i], reqs[i]) + }() + } + close(start) + wg.Wait() + + for i, rec := range recorders { + if rec.Code != http.StatusOK { + t.Fatalf("%s: concurrent request %d failed: %d %s", instanceType, i, rec.Code, rec.Body.String()) + } + got := rec.Header().Get(hash.ChecksumSHA256.Key()) + if want := mustChecksum(t, hash.ChecksumSHA256, data[i]); got != want { + t.Fatalf("%s: concurrent request %d checksum %q, want %q", instanceType, i, got, want) + } + // The ETag and the checksum must describe the same write, so a losing + // writer can never publish the winner's checksum next to its own ETag. + etags := rec.Header()[xhttp.ETag] + if len(etags) != 1 { + t.Fatalf("%s: concurrent request %d returned %d ETags", instanceType, i, len(etags)) + } + md5sum := md5.Sum(data[i]) + if want := hex.EncodeToString(md5sum[:]); canonicalizeETag(etags[0]) != want { + t.Fatalf("%s: concurrent request %d ETag %q, want %q", instanceType, i, etags[0], want) + } + } +} + +// federationTestDNS is a minimal dns.Store so a single test process can play +// both federation roles. +type federationTestDNS struct { + records map[string][]dns.SrvRecord +} + +func (f federationTestDNS) Put(string) error { return nil } + +func (f federationTestDNS) Get(bucket string) ([]dns.SrvRecord, error) { + records, ok := f.records[bucket] + if !ok { + return nil, dns.ErrNoEntriesFound + } + return records, nil +} + +func (f federationTestDNS) Delete(string) error { return nil } +func (f federationTestDNS) List() (map[string][]dns.SrvRecord, error) { return f.records, nil } +func (f federationTestDNS) DeleteRecord(dns.SrvRecord) error { return nil } +func (f federationTestDNS) Close() error { return nil } +func (f federationTestDNS) String() string { return "federation-test-dns" } + +// remoteBucketObjectLayer reports one existing bucket as missing so that +// isRemoteCopyRequired takes the legacy federation branch while the same +// process can still serve that bucket as the remote deployment. +type remoteBucketObjectLayer struct { + ObjectLayer + remoteBucket string +} + +func (l remoteBucketObjectLayer) GetBucketInfo(ctx context.Context, bucket string, opts BucketOptions) (BucketInfo, error) { + if bucket == l.remoteBucket { + return BucketInfo{}, toObjectErr(errVolumeNotFound, bucket) + } + return l.ObjectLayer.GetBucketInfo(ctx, bucket, opts) +} + +// TestAPIFederatedCopyObjectPartChecksum drives the legacy etcd federation +// branch of CopyObjectPartHandler end to end: the proxy forwards the copied +// bytes through the real getRemoteInstanceClient and minio-go, a second HTTP +// endpoint serves the real PutObjectPartHandler, and CopyPartResult must carry +// the checksum computed by that exact remote write. +func TestAPIFederatedCopyObjectPartChecksum(t *testing.T) { + defer DetectTestLeak(t)() + ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{ + t: t, + objAPITest: testAPIFederatedCopyObjectPartChecksum, + endpoints: []string{ + "CopyObjectPart", "NewMultipart", "PutObjectPart", + "ListObjectParts", "CompleteMultipart", "PutObject", + }, + }) +} + +func testAPIFederatedCopyObjectPartChecksum(objectAPI ObjectLayer, instanceType, bucketName string, + apiRouter http.Handler, credentials auth.Credentials, t *testing.T, +) { + algorithms := []struct { + name string + typ hash.ChecksumType + checksumType string + }{ + {name: "crc32-full-object", typ: hash.ChecksumCRC32, checksumType: xhttp.AmzChecksumTypeFullObject}, + {name: "sha256-composite", typ: hash.ChecksumSHA256, checksumType: xhttp.AmzChecksumTypeComposite}, + } + + data := bytes.Repeat([]byte("federated-upload-part-copy-"), 1024) + srcObject := "federation/copy-source.bin" + putCopyChecksumSource(t, apiRouter, credentials, bucketName, srcObject, data, nil) + + // The destination bucket really exists so the remote endpoint can serve it; + // only the proxy's own bucket lookup is told that it lives elsewhere. + remoteBucket := getRandomBucketName() + if err := objectAPI.MakeBucket(t.Context(), remoteBucket, MakeBucketOptions{}); err != nil { + t.Fatalf("%s: unable to create the remote bucket: %v", instanceType, err) + } + + remote := httptest.NewServer(apiRouter) + defer remote.Close() + host, port, _ := strings.Cut(remote.Listener.Addr().String(), ":") + + globalObjLayerMutex.Lock() + previousLayer := globalObjectAPI + globalObjectAPI = remoteBucketObjectLayer{ObjectLayer: previousLayer, remoteBucket: remoteBucket} + globalObjLayerMutex.Unlock() + previousDNS, previousFederation, previousIPs := globalDNSConfig, globalBucketFederation, globalDomainIPs + globalDNSConfig = federationTestDNS{records: map[string][]dns.SrvRecord{ + bucketName: {{Host: host, Port: json.Number(port)}}, + remoteBucket: {{Host: host, Port: json.Number(port)}}, + }} + // Every DNS record resolves to this process, so the bucket forwarding + // middleware always serves locally and only the handler proxies. + globalDomainIPs = set.CreateStringSet(remote.Listener.Addr().String()) + globalBucketFederation = true + defer func() { + globalObjLayerMutex.Lock() + globalObjectAPI = previousLayer + globalObjLayerMutex.Unlock() + globalDNSConfig, globalBucketFederation, globalDomainIPs = previousDNS, previousFederation, previousIPs + }() + + for _, algorithm := range algorithms { + t.Run(algorithm.name, func(t *testing.T) { + object := "federation/copy-destination-" + algorithm.name + ".bin" + uploadID := newMultipartUploadHTTP(t, apiRouter, credentials, remoteBucket, object, + algorithm.typ.String(), algorithm.checksumType) + + req, err := newTestSignedRequestV4(http.MethodPut, + getCopyObjectPartURL("", remoteBucket, object, uploadID, "1"), + 0, nil, credentials.AccessKey, credentials.SecretKey, + map[string]string{xhttp.AmzCopySource: SlashSeparator + pathJoin(bucketName, srcObject)}) + if err != nil { + t.Fatalf("%s: unable to build UploadPartCopy request: %v", instanceType, err) + } + rec := httptest.NewRecorder() + apiRouter.ServeHTTP(rec, req) + if rec.Code != http.StatusOK { + t.Fatalf("%s: federated UploadPartCopy failed: %d %s", instanceType, rec.Code, rec.Body.String()) + } + + var response CopyObjectPartResponse + if err := xml.Unmarshal(rec.Body.Bytes(), &response); err != nil { + t.Fatalf("%s: unable to decode CopyPartResult: %v", instanceType, err) + } + want := mustChecksum(t, algorithm.typ, data) + if got := copyPartChecksum(algorithm.typ, response); got != want { + t.Fatalf("%s: CopyPartResult %s is %q, want %q: %s", + instanceType, algorithm.typ.String(), got, want, rec.Body.String()) + } + + // The persisted part must carry the same value, and the client must be + // able to complete the upload with what CopyPartResult returned. + parts := listPartsHTTP(t, apiRouter, credentials, remoteBucket, object, uploadID, nil) + if len(parts.Parts) != 1 { + t.Fatalf("%s: ListParts returned %d parts, want 1", instanceType, len(parts.Parts)) + } + if got := partChecksum(algorithm.typ, parts.Parts[0]); got != want { + t.Fatalf("%s: persisted part %s is %q, want %q", instanceType, algorithm.typ.String(), got, want) + } + etag := canonicalizeETag(response.ETag) + completed := completePartsHTTP(t, apiRouter, credentials, remoteBucket, object, uploadID, + []CompletePart{completePartWithChecksum(algorithm.typ, 1, etag, want)}, nil) + if completed.Code != http.StatusOK { + t.Fatalf("%s: CompleteMultipartUpload rejected the federated part: %d %s", + instanceType, completed.Code, completed.Body.String()) + } + }) + } +} diff --git a/cmd/object-multipart-handlers.go b/cmd/object-multipart-handlers.go index fcc6d5b64..983644223 100644 --- a/cmd/object-multipart-handlers.go +++ b/cmd/object-multipart-handlers.go @@ -34,7 +34,6 @@ import ( "github.com/minio/minio-go/v7" "github.com/minio/minio-go/v7/pkg/encrypt" "github.com/minio/minio-go/v7/pkg/tags" - "github.com/minio/minio/internal/amztime" sse "github.com/minio/minio/internal/bucket/encryption" objectlock "github.com/minio/minio/internal/bucket/object/lock" "github.com/minio/minio/internal/bucket/replication" @@ -49,12 +48,94 @@ import ( xhttp "github.com/minio/minio/internal/http" "github.com/minio/minio/internal/logger" "github.com/minio/mux" - "github.com/minio/pkg/v3/policy" "github.com/minio/sio" + "github.com/pgsty/silo-pkg/v3/policy" ) // Multipart objectAPIHandlers +// isFederatedInternalRequest reports whether User-Agent carries the minio-go +// application token attached by getRemoteInstanceClient. +// +// This is only a response-shape hint. User-Agent is not authenticated and must +// never gate authorization, object visibility, or request validation. It is +// safe here because the only effect is returning the checksum of the body the +// caller was already authorized to upload. +func isFederatedInternalRequest(userAgent string) bool { + for _, product := range strings.Fields(userAgent) { + name, version, ok := strings.Cut(product, "/") + if ok && name == federatedInternalAppName && version != "" { + return true + } + } + return false +} + +// partChecksumMap returns the non-empty part checksums in the form expected by +// hash.AddChecksumHeader. x-amz-checksum-type is deliberately excluded because +// UploadPart does not return it and minio-go cannot carry it in ObjectPart. +func partChecksumMap(partInfo PartInfo) map[string]string { + checksums := make(map[string]string, 1) + if partInfo.ChecksumCRC32 != "" { + checksums[hash.ChecksumCRC32.String()] = partInfo.ChecksumCRC32 + } + if partInfo.ChecksumCRC32C != "" { + checksums[hash.ChecksumCRC32C.String()] = partInfo.ChecksumCRC32C + } + if partInfo.ChecksumSHA1 != "" { + checksums[hash.ChecksumSHA1.String()] = partInfo.ChecksumSHA1 + } + if partInfo.ChecksumSHA256 != "" { + checksums[hash.ChecksumSHA256.String()] = partInfo.ChecksumSHA256 + } + if partInfo.ChecksumCRC64NVME != "" { + checksums[hash.ChecksumCRC64NVME.String()] = partInfo.ChecksumCRC64NVME + } + return checksums +} + +// multipartChecksumType returns the base checksum type recorded when a +// multipart upload was created. The boolean reports whether an algorithm was +// recorded at all. +func multipartChecksumType(metadata map[string]string) (hash.ChecksumType, bool) { + algorithm := metadata[hash.MinIOMultipartChecksum] + if algorithm == "" { + return hash.ChecksumNone, false + } + t := hash.NewChecksumType(algorithm, metadata[hash.MinIOMultipartChecksumType]) + if !t.IsSet() { + return t, true + } + return t.Base(), true +} + +// prepareMultipartChecksumReader validates a supplied part checksum algorithm, +// or installs a server-side hasher when the client omitted the optional +// checksum. It must run before compression or encryption can consume reader. +func prepareMultipartChecksumReader(reader *hash.Reader, metadata map[string]string, bucket, object string) error { + want, ok := multipartChecksumType(metadata) + if !ok { + return nil + } + + got := reader.ContentCRCType() + if !got.IsSet() && reader.ServerSideChecksumType.IsSet() { + got = reader.ServerSideChecksumType + } + if !want.IsSet() || (got.IsSet() && got.Base() != want) { + return InvalidArgument{ + Bucket: bucket, + Object: object, + Err: fmt.Errorf("checksum missing, want %q, got %q", + metadata[hash.MinIOMultipartChecksum], got.String()), + } + } + if !got.IsSet() { + reader.AddServerSideChecksumHasher(want) + } + return nil +} + // NewMultipartUploadHandler - New multipart upload. // Notice: The S3 client can send secret keys in headers for encryption related jobs, // the handler should ensure to remove these keys before sending them to the object layer. @@ -89,12 +170,26 @@ func (api objectAPIHandlers) NewMultipartUploadHandler(w http.ResponseWriter, r writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL) return } + trustedReplication, replicaTrusted, trustErr := evaluateReplicationTrust(ctx, r, bucket, object, policy.ReplicateObjectAction) + if trustErr != ErrNone { + writeErrorResponse(ctx, w, errorCodes.ToAPIErr(trustErr), r.URL) + return + } + if hasReplicationRequestHeaders(r.Header) { + ctx, r = applyReplicationTrust(ctx, r, trustedReplication, replicaTrusted) + } - // Check if bucket encryption is enabled - sseConfig, _ := globalBucketSSEConfigSys.Get(bucket) - sseConfig.Apply(r.Header, sse.ApplyOptions{ - AutoEncrypt: globalAutoEncryption, - }) + // A validated raw SSE-C replica upload carries source ciphertext in every + // part; the destination must not add its own encryption or compression. + rawSSECReplica := isRawSSECReplica(r.Header, replicaTrusted) + + if !rawSSECReplica { + // Check if bucket encryption is enabled + sseConfig, _ := globalBucketSSEConfigSys.Get(bucket) + sseConfig.Apply(r.Header, sse.ApplyOptions{ + AutoEncrypt: globalAutoEncryption, + }) + } // Validate the storage class header if present. Query values retain the // existing compatibility path, including its historical validation behavior. @@ -123,20 +218,7 @@ func (api objectAPIHandlers) NewMultipartUploadHandler(w http.ResponseWriter, r return } - _, sourceReplReq := r.Header[xhttp.MinIOSourceReplicationRequest] - ssecRepHeaders := []string{ - "X-Minio-Replication-Server-Side-Encryption-Seal-Algorithm", - "X-Minio-Replication-Server-Side-Encryption-Sealed-Key", - "X-Minio-Replication-Server-Side-Encryption-Iv", - } - ssecRep := false - for _, header := range ssecRepHeaders { - if val := r.Header.Get(header); val != "" { - ssecRep = true - break - } - } - if !ssecRep || !sourceReplReq { + if !rawSSECReplica { if err = setEncryptionMetadata(r, bucket, object, encMetadata); err != nil { writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL) return @@ -160,30 +242,49 @@ func (api objectAPIHandlers) NewMultipartUploadHandler(w http.ResponseWriter, r return } } - if r.Header.Get(xhttp.AmzBucketReplicationStatus) == replication.Replica.String() { - if s3Err := isPutActionAllowed(ctx, getRequestAuthType(r), bucket, object, r, policy.ReplicateObjectAction); s3Err != ErrNone { - writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Err), r.URL) - return - } + if replicaTrusted { if err = extractReplicationMetadataFromMime(ctx, textproto.MIMEHeader(r.Header), metadata); err != nil { writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL) return } + metadata[xhttp.AmzBucketReplicationStatus] = replication.Replica.String() metadata[ReservedMetadataPrefixLower+ReplicaStatus] = replication.Replica.String() metadata[ReservedMetadataPrefixLower+ReplicaTimestamp] = UTCNow().Format(time.RFC3339Nano) + } else { + delete(metadata, xhttp.AmzBucketReplicationStatus) } retPerms := isPutActionAllowed(ctx, getRequestAuthType(r), bucket, object, r, policy.PutObjectRetentionAction) holdPerms := isPutActionAllowed(ctx, getRequestAuthType(r), bucket, object, r, policy.PutObjectLegalHoldAction) getObjectInfo := objectAPI.GetObjectInfo - retentionMode, retentionDate, legalHold, s3Err := checkPutObjectLockAllowed(ctx, r, bucket, object, getObjectInfo, retPerms, holdPerms) - if s3Err == ErrNone && retentionMode.Valid() { - metadata[strings.ToLower(xhttp.AmzObjectLockMode)] = string(retentionMode) - metadata[strings.ToLower(xhttp.AmzObjectLockRetainUntilDate)] = amztime.ISO8601Format(retentionDate.UTC()) - } - if s3Err == ErrNone && legalHold.Status.Valid() { - metadata[strings.ToLower(xhttp.AmzObjectLockLegalHold)] = string(legalHold.Status) + retentionMode, retentionDate, legalHold, s3Err := checkPutObjectLockAllowed(ctx, r, bucket, object, getObjectInfo, retPerms, holdPerms, replicaTrusted) + if s3Err == ErrNone { + // A trusted replica NewMultipartUpload addressing a specific version can + // be a full retransmit over an existing version whose lock state is newer + // than the source snapshot (issue #120). Order the incoming update against + // what is stored so a stale value cannot overwrite it. opts is built below + // (its ServerSideEncryption depends on the encMetadata merge that has not + // happened yet), so read the replica ordering inputs the way + // putOptsFromHeaders will; a malformed timestamp fails the request when + // opts is built, so a parse error here is left as a zero time. + var ( + storedLock objectLockState + srcRetentionTS, srcLegalholdTS time.Time + ) + if replicaTrusted { + srcRetentionTS, _ = time.Parse(time.RFC3339, strings.TrimSpace(r.Header.Get(xhttp.MinIOSourceObjectRetentionTimestamp))) + srcLegalholdTS, _ = time.Parse(time.RFC3339, strings.TrimSpace(r.Header.Get(xhttp.MinIOSourceObjectLegalHoldTimestamp))) + if versionID := strings.TrimSpace(r.Form.Get(xhttp.VersionID)); versionID != "" { + var lerr error + if storedLock, lerr = replicaStoredLock(ctx, getObjectInfo, bucket, object, versionID); lerr != nil { + writeErrorResponse(ctx, w, toAPIError(ctx, lerr), r.URL) + return + } + } + } + applyReplicatedObjectLock(metadata, storedLock, replicaTrusted, + retentionMode, retentionDate, legalHold, srcRetentionTS, srcLegalholdTS) } if s3Err != ErrNone { writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Err), r.URL) @@ -201,7 +302,7 @@ func (api objectAPIHandlers) NewMultipartUploadHandler(w http.ResponseWriter, r // Ensure that metadata does not contain sensitive information crypto.RemoveSensitiveEntries(metadata) - if isCompressible(r.Header, object) { + if !rawSSECReplica && isCompressible(r.Header, object) { // Storing the compression metadata. metadata[ReservedMetadataPrefix+"compression"] = compressionAlgorithmV2 } @@ -227,6 +328,10 @@ func (api objectAPIHandlers) NewMultipartUploadHandler(w http.ResponseWriter, r } } + if _, err := hash.GetContentChecksum(r.Header); err != nil { + writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrInvalidChecksum), r.URL) + return + } checksumType := hash.NewChecksumHeader(r.Header) if checksumType.Is(hash.ChecksumInvalid) { writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrInvalidChecksum), r.URL) @@ -322,6 +427,14 @@ func (api objectAPIHandlers) CopyObjectPartHandler(w http.ResponseWriter, r *htt writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL) return } + if hasReplicaStatus(r.Header) && + !replicationPermissionAllowed(ctx, r, dstBucket, dstObject, policy.ReplicateObjectAction) { + writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrAccessDenied), r.URL) + return + } + if hasReplicationRequestHeaders(r.Header) { + ctx, r = applyReplicationTrust(ctx, r, false, false) + } uploadID := r.Form.Get(xhttp.UploadID) partIDString := r.Form.Get(xhttp.PartNumber) @@ -465,7 +578,15 @@ func (api objectAPIHandlers) CopyObjectPartHandler(w http.ResponseWriter, r *htt return } - response := generateCopyObjectPartResponse(partInfo.ETag, partInfo.LastModified) + response := generateCopyObjectPartResponse(PartInfo{ + ETag: partInfo.ETag, + LastModified: partInfo.LastModified, + ChecksumCRC32: partInfo.ChecksumCRC32, + ChecksumCRC32C: partInfo.ChecksumCRC32C, + ChecksumSHA1: partInfo.ChecksumSHA1, + ChecksumSHA256: partInfo.ChecksumSHA256, + ChecksumCRC64NVME: partInfo.ChecksumCRC64NVME, + }) encodedSuccessResponse := encodeResponse(response) // Write success response. @@ -475,12 +596,25 @@ func (api objectAPIHandlers) CopyObjectPartHandler(w http.ResponseWriter, r *htt actualPartSize = length var reader io.Reader = etag.NewReader(ctx, gr, nil, nil) + var checksumReader *hash.Reader mi, err := objectAPI.GetMultipartInfo(ctx, dstBucket, dstObject, uploadID, dstOpts) if err != nil { writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL) return } + if _, ok := multipartChecksumType(mi.UserDefined); ok { + checksumReader, err = hash.NewReader(ctx, reader, length, "", "", actualPartSize) + if err != nil { + writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL) + return + } + if err = prepareMultipartChecksumReader(checksumReader, mi.UserDefined, dstBucket, dstObject); err != nil { + writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL) + return + } + reader = checksumReader + } _, isEncrypted := crypto.IsEncrypted(mi.UserDefined) @@ -512,6 +646,7 @@ func (api objectAPIHandlers) CopyObjectPartHandler(w http.ResponseWriter, r *htt rawReader := srcInfo.Reader pReader := NewPutObjReader(rawReader) + pReader.setChecksumReader(checksumReader) var objectEncryptionKey crypto.ObjectKey if isEncrypted { @@ -591,7 +726,7 @@ func (api objectAPIHandlers) CopyObjectPartHandler(w http.ResponseWriter, r *htt partInfo.ETag = tryDecryptETag(objectEncryptionKey[:], partInfo.ETag, sseS3) } - response := generateCopyObjectPartResponse(partInfo.ETag, partInfo.LastModified) + response := generateCopyObjectPartResponse(partInfo) encodedSuccessResponse := encodeResponse(response) // Write success response. @@ -741,10 +876,21 @@ func (api objectAPIHandlers) PutObjectPartHandler(w http.ResponseWriter, r *http writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL) return } + trustedReplication, _, trustErr := evaluateReplicationTrust(ctx, r, bucket, object, policy.ReplicateObjectAction) + if trustErr != ErrNone { + writeErrorResponse(ctx, w, errorCodes.ToAPIErr(trustErr), r.URL) + return + } + storedReplica := mi.UserDefined[xhttp.AmzBucketReplicationStatus] == replication.Replica.String() + replicaTrusted := trustedReplication && storedReplica + if hasReplicationRequestHeaders(r.Header) { + ctx, r = applyReplicationTrust(ctx, r, trustedReplication, replicaTrusted) + } // Read compression metadata preserved in the init multipart for the decision. _, isCompressed := mi.UserDefined[ReservedMetadataPrefix+"compression"] var idxCb func() []byte + var checksumReader *hash.Reader if isCompressed { actualReader, err := hash.NewReader(ctx, reader, size, md5hex, sha256hex, actualSize) if err != nil { @@ -755,6 +901,11 @@ func (api objectAPIHandlers) PutObjectPartHandler(w http.ResponseWriter, r *http writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrInvalidChecksum), r.URL) return } + if err = prepareMultipartChecksumReader(actualReader, mi.UserDefined, bucket, object); err != nil { + writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL) + return + } + checksumReader = actualReader // Set compression metrics. wantEncryption := crypto.Requested(r.Header) @@ -791,15 +942,21 @@ func (api objectAPIHandlers) PutObjectPartHandler(w http.ResponseWriter, r *http writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrInvalidChecksum), r.URL) return } + if checksumReader == nil { + if err = prepareMultipartChecksumReader(hashReader, mi.UserDefined, bucket, object); err != nil { + writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL) + return + } + checksumReader = hashReader + } pReader := NewPutObjReader(hashReader) + pReader.setChecksumReader(checksumReader) _, isEncrypted := crypto.IsEncrypted(mi.UserDefined) - _, replicationStatus := mi.UserDefined[xhttp.AmzBucketReplicationStatus] - _, sourceReplReq := r.Header[xhttp.MinIOSourceReplicationRequest] var objectEncryptionKey crypto.ObjectKey if isEncrypted { - if !crypto.SSEC.IsRequested(r.Header) && crypto.SSEC.IsEncrypted(mi.UserDefined) && !replicationStatus { + if !crypto.SSEC.IsRequested(r.Header) && crypto.SSEC.IsEncrypted(mi.UserDefined) && !replicaTrusted { writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrSSEMultipartEncrypted), r.URL) return } @@ -819,7 +976,7 @@ func (api objectAPIHandlers) PutObjectPartHandler(w http.ResponseWriter, r *http } } - if !sourceReplReq || !crypto.SSEC.IsEncrypted(mi.UserDefined) { + if !replicaTrusted || !crypto.SSEC.IsEncrypted(mi.UserDefined) { // Calculating object encryption key key, err = decryptObjectMeta(key, bucket, object, mi.UserDefined) if err != nil { @@ -878,7 +1035,7 @@ func (api objectAPIHandlers) PutObjectPartHandler(w http.ResponseWriter, r *http } opts.IndexCB = idxCb - opts.ReplicationRequest = sourceReplReq + opts.ReplicationRequest = trustedReplication putObjectPart := objectAPI.PutObjectPart partInfo, err := putObjectPart(ctx, bucket, object, uploadID, partID, pReader, opts) @@ -918,6 +1075,13 @@ func (api objectAPIHandlers) PutObjectPartHandler(w http.ResponseWriter, r *http // Therefore, we have to set the ETag directly as map entry. w.Header()[xhttp.ETag] = []string{"\"" + etag + "\""} hash.TransferChecksumHeader(w, r) + if isFederatedInternalRequest(r.UserAgent()) { + // Legacy federation proxies UploadPartCopy through minio-go + // Core.PutObjectPart, which can only recover checksums from response + // headers. Use the PartInfo returned by this exact write so the ETag and + // checksum cannot be mixed with a concurrent overwrite. + hash.AddChecksumHeader(w, partChecksumMap(partInfo)) + } writeSuccessResponseHeadersOnly(w) } @@ -946,6 +1110,14 @@ func (api objectAPIHandlers) CompleteMultipartUploadHandler(w http.ResponseWrite writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL) return } + trustedReplication, replicaTrusted, trustErr := evaluateReplicationTrust(ctx, r, bucket, object, policy.ReplicateObjectAction) + if trustErr != ErrNone { + writeErrorResponse(ctx, w, errorCodes.ToAPIErr(trustErr), r.URL) + return + } + if hasReplicationRequestHeaders(r.Header) { + ctx, r = applyReplicationTrust(ctx, r, trustedReplication, replicaTrusted) + } // Get upload id. uploadID, _, _, _, s3Error := getObjectResources(r.Form) @@ -988,7 +1160,7 @@ func (api objectAPIHandlers) CompleteMultipartUploadHandler(w http.ResponseWrite return } - if _, _, _, s3Err := checkPutObjectLockAllowed(ctx, r, bucket, object, objectAPI.GetObjectInfo, ErrNone, ErrNone); s3Err != ErrNone { + if _, _, _, s3Err := checkPutObjectLockAllowed(ctx, r, bucket, object, objectAPI.GetObjectInfo, ErrNone, ErrNone, false); s3Err != ErrNone { writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Err), r.URL) return } @@ -1013,6 +1185,14 @@ func (api objectAPIHandlers) CompleteMultipartUploadHandler(w http.ResponseWrite } opts.Versioned = versioned opts.VersionSuspended = suspended + // A replicated multipart completion carries the internal replication marker + // (the sender does not re-assert REPLICA status on Complete, so this is keyed + // on trusted replication, matching completeMultipartOpts). The object layer + // re-orders the Object Lock it carries against the destination version read + // under the write lock, but only for an SSE-C upload -- the scope this issue + // enables -- so a marker-only non-SSE-C completion keeps ordinary write + // semantics (issue #120). + opts.ReplicaLockReconcile = trustedReplication // First, we compute the ETag of the multipart object. // The ETag of a multi-part object is always: @@ -1071,7 +1251,7 @@ func (api objectAPIHandlers) CompleteMultipartUploadHandler(w http.ResponseWrite if dsc := mustReplicate(ctx, bucket, object, objInfo.getMustReplicateOptions(replication.ObjectReplicationType, opts)); dsc.ReplicateAny() { scheduleReplication(ctx, objInfo, objectAPI, dsc, replication.ObjectReplicationType) } - if _, ok := r.Header[xhttp.MinIOSourceReplicationRequest]; ok { + if isTrustedReplication(ctx) { actualSize, _ := objInfo.GetActualSize() defer globalReplicationStats.Load().UpdateReplicaStat(bucket, actualSize) } diff --git a/cmd/object-ssec-zero-byte_test.go b/cmd/object-ssec-zero-byte_test.go new file mode 100644 index 000000000..787c11893 --- /dev/null +++ b/cmd/object-ssec-zero-byte_test.go @@ -0,0 +1,226 @@ +// Copyright (c) 2015-2026 MinIO, Inc. +// +// This file is part of MinIO Object Storage stack +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package cmd + +import ( + "bytes" + "crypto/md5" + "encoding/base64" + "encoding/xml" + "net/http" + "net/http/httptest" + "testing" + + "github.com/minio/minio/internal/auth" + xhttp "github.com/minio/minio/internal/http" +) + +func TestAPIZeroByteSSECAuthenticatesKey(t *testing.T) { + defer DetectTestLeak(t)() + ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{ + t: t, + objAPITest: testAPIZeroByteSSECAuthenticatesKey, + endpoints: []string{"CopyObject", "CopyObjectPart", "PutObject", "GetObject", "HeadObject", "NewMultipart"}, + }) +} + +func testAPIZeroByteSSECAuthenticatesKey(obj ObjectLayer, instanceType, bucketName string, + apiRouter http.Handler, credentials auth.Credentials, t *testing.T, +) { + previousTLS := globalIsTLS + globalIsTLS = true + defer func() { globalIsTLS = previousTLS }() + + object := "ssec/zero-byte" + oldKey := bytes.Repeat([]byte{0x11}, 32) + oldMD5 := md5.Sum(oldKey) + wrongKey := bytes.Repeat([]byte{0x22}, 32) + wrongMD5 := md5.Sum(wrongKey) + putCopyChecksumSource(t, apiRouter, credentials, bucketName, object, nil, map[string]string{ + xhttp.AmzServerSideEncryptionCustomerAlgorithm: xhttp.AmzEncryptionAES, + xhttp.AmzServerSideEncryptionCustomerKey: base64.StdEncoding.EncodeToString(oldKey), + xhttp.AmzServerSideEncryptionCustomerKeyMD5: base64.StdEncoding.EncodeToString(oldMD5[:]), + }) + + correctHeaders := map[string]string{ + xhttp.AmzServerSideEncryptionCustomerAlgorithm: xhttp.AmzEncryptionAES, + xhttp.AmzServerSideEncryptionCustomerKey: base64.StdEncoding.EncodeToString(oldKey), + xhttp.AmzServerSideEncryptionCustomerKeyMD5: base64.StdEncoding.EncodeToString(oldMD5[:]), + } + wrongHeaders := map[string]string{ + xhttp.AmzServerSideEncryptionCustomerAlgorithm: xhttp.AmzEncryptionAES, + xhttp.AmzServerSideEncryptionCustomerKey: base64.StdEncoding.EncodeToString(wrongKey), + xhttp.AmzServerSideEncryptionCustomerKeyMD5: base64.StdEncoding.EncodeToString(wrongMD5[:]), + } + + if rec := ssecZeroByteRequest(t, apiRouter, credentials, http.MethodGet, bucketName, object, correctHeaders); rec.Code != http.StatusOK || rec.Body.Len() != 0 { + t.Fatalf("%s: correct-key GET returned %d with %d bytes: %s", instanceType, rec.Code, rec.Body.Len(), rec.Body.String()) + } + headRec := ssecZeroByteRequest(t, apiRouter, credentials, http.MethodHead, bucketName, object, correctHeaders) + if headRec.Code != http.StatusOK { + t.Fatalf("%s: correct-key HEAD returned %d", instanceType, headRec.Code) + } + if rec := ssecZeroByteRequest(t, apiRouter, credentials, http.MethodGet, bucketName, object, wrongHeaders); rec.Code != http.StatusForbidden { + t.Fatalf("%s: wrong-key GET returned %d, want %d: %s", instanceType, rec.Code, http.StatusForbidden, rec.Body.String()) + } + if rec := ssecZeroByteRequest(t, apiRouter, credentials, http.MethodHead, bucketName, object, wrongHeaders); rec.Code != http.StatusForbidden { + t.Fatalf("%s: wrong-key HEAD returned %d, want %d", instanceType, rec.Code, http.StatusForbidden) + } + conditionalHeaders := make(map[string]string, len(wrongHeaders)+1) + for key, value := range wrongHeaders { + conditionalHeaders[key] = value + } + conditionalInfo, err := obj.GetObjectInfo(t.Context(), bucketName, object, ObjectOptions{}) + if err != nil { + t.Fatal(err) + } + conditionalRequest := httptest.NewRequest(http.MethodGet, getGetObjectURL("", bucketName, object), nil) + for key, value := range wrongHeaders { + conditionalRequest.Header.Set(key, value) + } + if _, err := DecryptObjectInfo(&conditionalInfo, conditionalRequest); err != nil { + t.Fatal(err) + } + conditionalHeaders[xhttp.IfNoneMatch] = conditionalInfo.ETag + if rec := ssecZeroByteRequest(t, apiRouter, credentials, http.MethodGet, bucketName, object, conditionalHeaders); rec.Code != http.StatusNotModified { + t.Fatalf("%s: conditional wrong-key GET returned %d, want %d: %s", instanceType, rec.Code, http.StatusNotModified, rec.Body.String()) + } + if rec := ssecZeroByteRequest(t, apiRouter, credentials, http.MethodGet, bucketName, object, nil); rec.Code != http.StatusBadRequest { + t.Fatalf("%s: missing-key GET returned %d, want %d: %s", instanceType, rec.Code, http.StatusBadRequest, rec.Body.String()) + } + + nonEmptyObject := "ssec/one-byte" + putCopyChecksumSource(t, apiRouter, credentials, bucketName, nonEmptyObject, []byte{1}, correctHeaders) + if rec := ssecZeroByteRequest(t, apiRouter, credentials, http.MethodGet, bucketName, nonEmptyObject, wrongHeaders); rec.Code != http.StatusForbidden { + t.Fatalf("%s: one-byte wrong-key GET returned %d, want %d: %s", instanceType, rec.Code, http.StatusForbidden, rec.Body.String()) + } + + plainObject := "ssec/plain-zero-byte" + putCopyChecksumSource(t, apiRouter, credentials, bucketName, plainObject, nil, nil) + if rec := ssecZeroByteRequest(t, apiRouter, credentials, http.MethodGet, bucketName, plainObject, wrongHeaders); rec.Code != http.StatusBadRequest { + t.Fatalf("%s: unencrypted wrong-key GET returned %d, want %d: %s", instanceType, rec.Code, http.StatusBadRequest, rec.Body.String()) + } + + destination := "ssec/zero-byte-copy" + rec := copyChecksumRequest(t, apiRouter, credentials, bucketName, object, destination, map[string]string{ + xhttp.AmzServerSideEncryptionCopyCustomerAlgorithm: xhttp.AmzEncryptionAES, + xhttp.AmzServerSideEncryptionCopyCustomerKey: base64.StdEncoding.EncodeToString(wrongKey), + xhttp.AmzServerSideEncryptionCopyCustomerKeyMD5: base64.StdEncoding.EncodeToString(wrongMD5[:]), + }) + if rec.Code != http.StatusForbidden { + t.Fatalf("%s: wrong-key CopyObject returned %d, want %d: %s", instanceType, rec.Code, http.StatusForbidden, rec.Body.String()) + } + if _, err := obj.GetObjectInfo(t.Context(), bucketName, destination, ObjectOptions{}); !isErrObjectNotFound(err) { + t.Fatalf("%s: rejected CopyObject created the destination: %v", instanceType, err) + } + + rec = copyChecksumRequest(t, apiRouter, credentials, bucketName, object, object, map[string]string{ + xhttp.AmzStorageClass: "REDUCED_REDUNDANCY", + xhttp.AmzServerSideEncryptionCopyCustomerAlgorithm: xhttp.AmzEncryptionAES, + xhttp.AmzServerSideEncryptionCopyCustomerKey: base64.StdEncoding.EncodeToString(wrongKey), + xhttp.AmzServerSideEncryptionCopyCustomerKeyMD5: base64.StdEncoding.EncodeToString(wrongMD5[:]), + }) + if rec.Code != http.StatusForbidden { + t.Fatalf("%s: wrong-key storage-class CopyObject returned %d, want %d: %s", instanceType, rec.Code, http.StatusForbidden, rec.Body.String()) + } + + multipartObject := "ssec/zero-byte-multipart-copy" + req, err := newTestSignedRequestV4(http.MethodPost, getNewMultipartURL("", bucketName, multipartObject), + 0, nil, credentials.AccessKey, credentials.SecretKey, nil) + if err != nil { + t.Fatal(err) + } + rec = httptest.NewRecorder() + apiRouter.ServeHTTP(rec, req) + if rec.Code != http.StatusOK { + t.Fatalf("%s: NewMultipartUpload returned %d: %s", instanceType, rec.Code, rec.Body.String()) + } + var initiated InitiateMultipartUploadResponse + if err := xml.Unmarshal(rec.Body.Bytes(), &initiated); err != nil { + t.Fatal(err) + } + req, err = newTestSignedRequestV4(http.MethodPut, + getCopyObjectPartURL("", bucketName, multipartObject, initiated.UploadID, "1"), + 0, nil, credentials.AccessKey, credentials.SecretKey, map[string]string{ + xhttp.AmzServerSideEncryptionCopyCustomerAlgorithm: xhttp.AmzEncryptionAES, + xhttp.AmzServerSideEncryptionCopyCustomerKey: base64.StdEncoding.EncodeToString(wrongKey), + xhttp.AmzServerSideEncryptionCopyCustomerKeyMD5: base64.StdEncoding.EncodeToString(wrongMD5[:]), + }) + if err != nil { + t.Fatal(err) + } + req.Header.Set(xhttp.AmzCopySource, SlashSeparator+pathJoin(bucketName, object)) + rec = httptest.NewRecorder() + apiRouter.ServeHTTP(rec, req) + if rec.Code != http.StatusForbidden { + t.Fatalf("%s: wrong-key UploadPartCopy returned %d, want %d: %s", instanceType, rec.Code, http.StatusForbidden, rec.Body.String()) + } + parts, err := obj.ListObjectParts(t.Context(), bucketName, multipartObject, initiated.UploadID, 0, 1000, ObjectOptions{}) + if err != nil { + t.Fatal(err) + } + if len(parts.Parts) != 0 { + t.Fatalf("%s: rejected UploadPartCopy stored %d parts", instanceType, len(parts.Parts)) + } + if err := obj.AbortMultipartUpload(t.Context(), bucketName, multipartObject, initiated.UploadID, ObjectOptions{}); err != nil { + t.Fatal(err) + } + + wrongHeader := http.Header{} + for key, value := range wrongHeaders { + wrongHeader.Set(key, value) + } + for _, test := range []struct { + header http.Header + opts ObjectOptions + }{ + {header: nil, opts: ObjectOptions{}}, + {header: wrongHeader, opts: ObjectOptions{NoDecryption: true}}, + {header: wrongHeader, opts: ObjectOptions{ReplicationRequest: true}}, + {header: wrongHeader, opts: ObjectOptions{Transition: TransitionOptions{RestoreRequest: &RestoreObjectRequest{}}}}, + } { + gr, err := obj.GetObjectNInfo(t.Context(), bucketName, object, nil, test.header, test.opts) + if err != nil { + t.Fatalf("%s: internal zero-byte read with opts %+v failed: %v", instanceType, test.opts, err) + } + gr.Close() + } + + rangeHeaders := make(map[string]string, len(wrongHeaders)+1) + for key, value := range wrongHeaders { + rangeHeaders[key] = value + } + rangeHeaders[xhttp.Range] = "bytes=0-0" + if rec := ssecZeroByteRequest(t, apiRouter, credentials, http.MethodGet, bucketName, object, rangeHeaders); rec.Code != http.StatusRequestedRangeNotSatisfiable { + t.Fatalf("%s: ranged wrong-key GET returned %d, want %d: %s", instanceType, rec.Code, http.StatusRequestedRangeNotSatisfiable, rec.Body.String()) + } +} + +func ssecZeroByteRequest(t *testing.T, apiRouter http.Handler, credentials auth.Credentials, + method, bucket, object string, headers map[string]string, +) *httptest.ResponseRecorder { + t.Helper() + req, err := newTestSignedRequestV4(method, getGetObjectURL("", bucket, object), + 0, nil, credentials.AccessKey, credentials.SecretKey, headers) + if err != nil { + t.Fatal(err) + } + rec := httptest.NewRecorder() + apiRouter.ServeHTTP(rec, req) + return rec +} diff --git a/cmd/object_api_suite_test.go b/cmd/object_api_suite_test.go index 3377ad208..0e037295d 100644 --- a/cmd/object_api_suite_test.go +++ b/cmd/object_api_suite_test.go @@ -849,6 +849,45 @@ func testListObjectsTestsForNonExistentBucket(obj ObjectLayer, instanceType stri } } +// Wrapper for calling testListObjectsShortcutsForNonExistentBucket for both +// single-drive and multi-drive erasure setups. +func TestListObjectsShortcutsForNonExistentBucket(t *testing.T) { + ExecObjectLayerTest(t, testListObjectsShortcutsForNonExistentBucket) +} + +// Tests validate that storage-bypassing list shortcuts do not mask a missing +// bucket as an empty result. The regular prefix case is a control for the +// storage-backed listing path. +func testListObjectsShortcutsForNonExistentBucket(obj ObjectLayer, instanceType string, t TestErrHandler) { + testCases := []struct { + name string + prefix string + marker string + maxKeys int + }{ + {name: "slash-prefixed prefix", prefix: "/", maxKeys: 1000}, + {name: "zero limit", prefix: "obj", maxKeys: 0}, + {name: "marker outside prefix", prefix: "a", marker: "b", maxKeys: 1000}, + {name: "regular prefix", prefix: "foo/bar", maxKeys: 1000}, + } + for _, tc := range testCases { + _, err := obj.ListObjects(context.Background(), "bucket", tc.prefix, tc.marker, "", tc.maxKeys) + if !isErrBucketNotFound(err) { + t.Errorf("%s: ListObjects %s: expected BucketNotFound, got %v", instanceType, tc.name, err) + } + + _, err = obj.ListObjectsV2(context.Background(), "bucket", tc.prefix, "", "", tc.maxKeys, false, tc.marker) + if !isErrBucketNotFound(err) { + t.Errorf("%s: ListObjectsV2 %s: expected BucketNotFound, got %v", instanceType, tc.name, err) + } + + _, err = obj.ListObjectVersions(context.Background(), "bucket", tc.prefix, tc.marker, "", "", tc.maxKeys) + if !isErrBucketNotFound(err) { + t.Errorf("%s: ListObjectVersions %s: expected BucketNotFound, got %v", instanceType, tc.name, err) + } + } +} + // Wrapper for calling testNonExistentObjectInBucket for both Erasure and FS. func TestNonExistentObjectInBucket(t *testing.T) { ExecObjectLayerTest(t, testNonExistentObjectInBucket) diff --git a/cmd/os-readdir_test.go b/cmd/os-readdir_test.go index 5649b5391..a43ad5488 100644 --- a/cmd/os-readdir_test.go +++ b/cmd/os-readdir_test.go @@ -80,8 +80,6 @@ func setupTestReadDirFiles(t *testing.T) (testResults []result) { for i := range 10 { name := fmt.Sprintf("file-%d", i) if err := os.WriteFile(filepath.Join(dir, name), []byte{}, os.ModePerm); err != nil { - // For cleanup, its required to add these entries into test results. - testResults = append(testResults, result{dir, entries}) t.Fatalf("Unable to create file, %s", err) } entries = append(entries, name) @@ -105,8 +103,6 @@ func setupTestReadDirGeneric(t *testing.T) (testResults []result) { for i := range 10 { name := fmt.Sprintf("file-%d", i) if err := os.WriteFile(filepath.Join(dir, "mydir", name), []byte{}, os.ModePerm); err != nil { - // For cleanup, its required to add these entries into test results. - testResults = append(testResults, result{dir, entries}) t.Fatalf("Unable to write file, %s", err) } } @@ -130,8 +126,6 @@ func setupTestReadDirSymlink(t *testing.T) (testResults []result) { name1 := fmt.Sprintf("file-%d", i) name2 := fmt.Sprintf("file-%d", i+10) if err := os.WriteFile(filepath.Join(dir, name1), []byte{}, os.ModePerm); err != nil { - // For cleanup, its required to add these entries into test results. - testResults = append(testResults, result{dir, entries}) t.Fatalf("Unable to create a file, %s", err) } // Symlink will not be added to entries. diff --git a/cmd/peer-rest-client.go b/cmd/peer-rest-client.go index eb65a40a0..082b0a36c 100644 --- a/cmd/peer-rest-client.go +++ b/cmd/peer-rest-client.go @@ -36,7 +36,7 @@ import ( xhttp "github.com/minio/minio/internal/http" "github.com/minio/minio/internal/logger" "github.com/minio/minio/internal/rest" - xnet "github.com/minio/pkg/v3/net" + xnet "github.com/pgsty/silo-pkg/v3/net" ) // client to talk to peer Nodes. diff --git a/cmd/peer-s3-client.go b/cmd/peer-s3-client.go index 438b4d336..5b30f76b2 100644 --- a/cmd/peer-s3-client.go +++ b/cmd/peer-s3-client.go @@ -29,7 +29,7 @@ import ( "github.com/minio/madmin-go/v3" "github.com/minio/minio/internal/grid" - "github.com/minio/pkg/v3/sync/errgroup" + "github.com/pgsty/silo-pkg/v3/sync/errgroup" ) var errPeerOffline = errors.New("peer is offline") diff --git a/cmd/peer-s3-server.go b/cmd/peer-s3-server.go index 227aebe8d..a252ee85f 100644 --- a/cmd/peer-s3-server.go +++ b/cmd/peer-s3-server.go @@ -22,7 +22,7 @@ import ( "errors" "github.com/minio/madmin-go/v3" - "github.com/minio/pkg/v3/sync/errgroup" + "github.com/pgsty/silo-pkg/v3/sync/errgroup" "github.com/puzpuzpuz/xsync/v3" ) diff --git a/cmd/perf-tests.go b/cmd/perf-tests.go index f9b4663f4..f8fb7df8d 100644 --- a/cmd/perf-tests.go +++ b/cmd/perf-tests.go @@ -36,7 +36,7 @@ import ( "github.com/minio/minio-go/v7/pkg/credentials" xhttp "github.com/minio/minio/internal/http" xioutil "github.com/minio/minio/internal/ioutil" - "github.com/minio/pkg/v3/randreader" + "github.com/pgsty/silo-pkg/v3/randreader" ) // SpeedTestResult return value of the speedtest function diff --git a/cmd/policy_test.go b/cmd/policy_test.go index bd9c9add5..1c0c8ea2e 100644 --- a/cmd/policy_test.go +++ b/cmd/policy_test.go @@ -23,8 +23,8 @@ import ( miniogopolicy "github.com/minio/minio-go/v7/pkg/policy" "github.com/minio/minio-go/v7/pkg/set" - "github.com/minio/pkg/v3/policy" - "github.com/minio/pkg/v3/policy/condition" + "github.com/pgsty/silo-pkg/v3/policy" + "github.com/pgsty/silo-pkg/v3/policy/condition" ) func TestPolicySysIsAllowed(t *testing.T) { diff --git a/cmd/post-policy_test.go b/cmd/post-policy_test.go index 9a02ca846..7fb84dbd6 100644 --- a/cmd/post-policy_test.go +++ b/cmd/post-policy_test.go @@ -33,6 +33,7 @@ import ( "time" "github.com/dustin/go-humanize" + xhttp "github.com/minio/minio/internal/http" ) const ( @@ -184,6 +185,49 @@ func TestPostPolicyBucketHandler(t *testing.T) { ExecObjectLayerTest(t, testPostPolicyBucketHandler) } +func TestPostPolicyCannotForgeReplicationStatus(t *testing.T) { + ExecObjectLayerTest(t, testPostPolicyCannotForgeReplicationStatus) +} + +func testPostPolicyCannotForgeReplicationStatus(obj ObjectLayer, instanceType string, t TestErrHandler) { + if err := newTestConfig(globalMinioDefaultRegion, obj); err != nil { + t.Fatalf("Initializing config.json failed") + } + bucketName := getRandomBucketName() + if err := obj.MakeBucket(context.Background(), bucketName, MakeBucketOptions{}); err != nil { + t.Fatalf("%s: make bucket: %v", instanceType, err) + } + apiRouter := initTestAPIEndPoints(obj, []string{"PostPolicy"}) + credentials := globalActiveCred + now := UTCNow() + region := globalMinioDefaultRegion + objectPrefix := "post-policy-replication-status" + policyBytes := buildGenericPolicy(now, credentials.AccessKey, region, bucketName, objectPrefix, false) + policyText := strings.TrimSuffix(string(policyBytes), "]}") + + `,["eq","$x-amz-replication-status","REPLICA"]]}` + req, err := newPostRequestV4Generic("", bucketName, objectPrefix, []byte("post policy payload"), + credentials.AccessKey, credentials.SecretKey, region, now, []byte(policyText), + map[string]string{xhttp.AmzBucketReplicationStatus: "REPLICA"}, false, false, false) + if err != nil { + t.Fatalf("%s: create post request: %v", instanceType, err) + } + rec := httptest.NewRecorder() + apiRouter.ServeHTTP(rec, req) + if rec.Code != http.StatusNoContent { + t.Fatalf("%s: POST status %d: %s", instanceType, rec.Code, rec.Body.String()) + } + info, err := obj.GetObjectInfo(context.Background(), bucketName, objectPrefix+"/upload.txt", ObjectOptions{}) + if err != nil { + t.Fatalf("%s: get object info: %v", instanceType, err) + } + if got := info.UserDefined[xhttp.AmzBucketReplicationStatus]; got != "" { + t.Fatalf("%s: forged replication status persisted as %q", instanceType, got) + } + if !info.ReplicationStatus.Empty() { + t.Fatalf("%s: forged replication status reached ObjectInfo: %q", instanceType, info.ReplicationStatus) + } +} + // testPostPolicyBucketHandler - Tests validate post policy handler uploading objects. func testPostPolicyBucketHandler(obj ObjectLayer, instanceType string, t TestErrHandler) { if err := newTestConfig(globalMinioDefaultRegion, obj); err != nil { diff --git a/cmd/replication-ssec-retransmit_test.go b/cmd/replication-ssec-retransmit_test.go new file mode 100644 index 000000000..c845b4668 --- /dev/null +++ b/cmd/replication-ssec-retransmit_test.go @@ -0,0 +1,1944 @@ +// Copyright (c) 2015-2026 MinIO, Inc. +// Copyright (c) 2026 PGSTY +// +// This file is part of MinIO Object Storage stack +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +package cmd + +import ( + "bytes" + "context" + "crypto/md5" + "encoding/base64" + "encoding/xml" + "io" + "maps" + "net/http" + "net/http/httptest" + "net/url" + "slices" + "strconv" + "strings" + "testing" + "time" + + "github.com/minio/minio/internal/auth" + "github.com/minio/minio/internal/crypto" + xhttp "github.com/minio/minio/internal/http" + "github.com/minio/sio" +) + +// TestAPISSECReplicationTargetHead pins what the replication sender's target +// HEAD sees for an SSE-C object, which is what replicateAll's dispatch relies +// on: a keyless HEAD answers 400 InvalidRequest (so the sender must retransmit +// rather than compare), a missing key still answers 404 NoSuchKey (so a missing +// replica keeps healing), a HEAD carrying the internal replication marker +// answers with the replica metadata (what the resync accounting HEAD now +// sends), and the metadata-only CopyObject the sender used to fall into fails +// with ExcessData on any non-empty object. See pgsty/silo#120. +func TestAPISSECReplicationTargetHead(t *testing.T) { + defer DetectTestLeak(t)() + ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{ + t: t, + objAPITest: testAPISSECReplicationTargetHead, + makeBucketOptions: MakeBucketOptions{VersioningEnabled: true}, + }) +} + +func testAPISSECReplicationTargetHead(obj ObjectLayer, instanceType, bucketName string, + apiRouter http.Handler, credentials auth.Credentials, t *testing.T, +) { + previousTLS := globalIsTLS + globalIsTLS = true + defer func() { globalIsTLS = previousTLS }() + + key := bytes.Repeat([]byte{0x42}, 32) + keyMD5 := md5.Sum(key) + data := bytes.Repeat([]byte("ssec-keyless-head-"), 512) + sseHeaders := map[string]string{ + xhttp.AmzServerSideEncryptionCustomerAlgorithm: xhttp.AmzEncryptionAES, + xhttp.AmzServerSideEncryptionCustomerKey: base64.StdEncoding.EncodeToString(key), + xhttp.AmzServerSideEncryptionCustomerKeyMD5: base64.StdEncoding.EncodeToString(keyMD5[:]), + } + object := "ssec-keyless-head/replica" + putCopyChecksumSource(t, apiRouter, credentials, bucketName, object, data, sseHeaders) + + // The replication target credential holds the standard replication actions. + replicator := newObjectAttributesAuthzUser(t, instanceType, bucketName, + `"s3:GetObject","s3:PutObject","s3:ReplicateObject","s3:ReplicateDelete","s3:ReplicateTags"`) + + // Exactly the header set replicateAll's StatObject sends today. + senderHeaders := map[string]string{ + "X-Minio-Source-Proxy-Request": "false", + xhttp.AmzTagDirective: "ACCESS", + } + // The same request with the internal replication marker added. + markedHeaders := map[string]string{ + "X-Minio-Source-Proxy-Request": "false", + xhttp.AmzTagDirective: "ACCESS", + xhttp.MinIOSourceReplicationRequest: "true", + } + + head := func(t *testing.T, creds auth.Credentials, obj, versionID string, headers map[string]string) *httptest.ResponseRecorder { + t.Helper() + headURL := getGetObjectURL("", bucketName, obj) + if versionID != "" { + // replicateAll addresses the source version (minio-go + // api-get-options.go toQueryValues). + headURL += "?versionId=" + versionID + } + req, err := newTestSignedRequestV4(http.MethodHead, headURL, 0, nil, + creds.AccessKey, creds.SecretKey, headers) + if err != nil { + t.Fatal(err) + } + rec := httptest.NewRecorder() + apiRouter.ServeHTTP(rec, req) + return rec + } + + sseDesc := errorCodes[ErrSSEEncryptedObject].Description + + baseInfo, err := obj.GetObjectInfo(t.Context(), bucketName, object, ObjectOptions{}) + if err != nil { + t.Fatal(err) + } + sourceVersion := baseInfo.VersionID + + t.Run("sender-head-today-is-rejected", func(t *testing.T) { + rec := head(t, replicator, object, sourceVersion, senderHeaders) + if rec.Code != http.StatusBadRequest { + t.Fatalf("%s: keyless HEAD status %d, want 400", instanceType, rec.Code) + } + if got := rec.Header().Get("x-minio-error-code"); got != "InvalidRequest" { + t.Fatalf("%s: error code %q, want InvalidRequest", instanceType, got) + } + desc := strings.Trim(rec.Header().Get("x-minio-error-desc"), `"`) + if !strings.Contains(desc, sseDesc) { + t.Fatalf("%s: error desc %q does not carry %q", instanceType, desc, sseDesc) + } + t.Logf("%s: keyless HEAD -> %d %s / %s", instanceType, rec.Code, + rec.Header().Get("x-minio-error-code"), desc) + }) + + t.Run("missing-object-head-is-distinguishable", func(t *testing.T) { + rec := head(t, replicator, "ssec-keyless-head/absent", "", senderHeaders) + if rec.Code != http.StatusNotFound { + t.Fatalf("%s: missing-object HEAD status %d, want 404", instanceType, rec.Code) + } + if got := rec.Header().Get("x-minio-error-code"); got != "NoSuchKey" { + t.Fatalf("%s: missing-object error code %q, want NoSuchKey", instanceType, got) + } + }) + + t.Run("marked-head-answers-with-metadata", func(t *testing.T) { + rec := head(t, replicator, object, sourceVersion, markedHeaders) + if rec.Code != http.StatusOK { + t.Fatalf("%s: marked keyless HEAD status %d, want 200: %s", instanceType, rec.Code, rec.Body.String()) + } + // setObjectHeaders assigns the ETag through the raw header map with the + // non canonical key "ETag", so read it the same way. + etag := "" + if v := rec.Header()[xhttp.ETag]; len(v) > 0 { + etag = strings.Trim(v[0], `"`) + } + clen := rec.Header().Get(xhttp.ContentLength) + lastMod := rec.Header().Get(xhttp.LastModified) + if etag == "" || clen == "" || lastMod == "" { + t.Fatalf("%s: marked HEAD lacks comparison metadata etag=%q len=%q mtime=%q", + instanceType, etag, clen, lastMod) + } + + // What replicateAll would compare this against: the source ObjectInfo it + // obtained from GetObjectNInfo(..., ReplicationRequest: true). + gr, err := obj.GetObjectNInfo(t.Context(), bucketName, object, nil, http.Header{}, ObjectOptions{ + ReplicationRequest: true, + }) + if err != nil { + t.Fatal(err) + } + srcInfo := gr.ObjInfo + gr.Close() + srcSize, err := srcInfo.GetActualSize() + if err != nil { + t.Fatal(err) + } + headSize, err := strconv.ParseInt(clen, 10, 64) + if err != nil { + t.Fatal(err) + } + t.Logf("%s: source ETag=%q (len %d) size=%d ; target HEAD ETag=%q (len %d) size=%d", + instanceType, srcInfo.ETag, len(srcInfo.ETag), srcSize, etag, len(etag), headSize) + if headSize != srcSize { + t.Errorf("%s: getReplicationAction size mismatch: source %d target %d", instanceType, srcSize, headSize) + } + if srcInfo.ETag != etag { + t.Logf("%s: NOTE getReplicationAction would see an ETag mismatch (source keeps the sealed ETag, "+ + "the target HEAD returns the last 32 bytes) and therefore return replicateAll", instanceType) + } + }) + + t.Run("zero-byte-metadata-copy-succeeds", func(t *testing.T) { + // A zero-byte SSE-C object has nothing for the plaintext-sized reader to + // overrun, so the same copy request succeeds. The ExcessData failure is a + // property of non-empty objects. + zero := "ssec-keyless-head/zero" + putCopyChecksumSource(t, apiRouter, credentials, bucketName, zero, nil, sseHeaders) + gr, err := obj.GetObjectNInfo(t.Context(), bucketName, zero, nil, http.Header{}, ObjectOptions{ + ReplicationRequest: true, + }) + if err != nil { + t.Fatal(err) + } + zi := gr.ObjInfo + gr.Close() + + copySrc := url.QueryEscape(SlashSeparator+bucketName+SlashSeparator+zero) + "?versionId=" + zi.VersionID + headers := map[string]string{ + xhttp.AmzCopySource: copySrc, + xhttp.MinIOSourceReplicationRequest: "true", + } + for k, v := range getCopyObjMetadata(zi, "") { + if strings.EqualFold(k, "content-length") { + continue + } + headers[k] = v + } + headers[xhttp.AmzObjectTagging] = "keyless-head=zero" + req, err := newTestSignedRequestV4(http.MethodPut, + getCopyObjectURL("", bucketName, zero)+"?versionId="+zi.VersionID, 0, nil, + replicator.AccessKey, replicator.SecretKey, headers) + if err != nil { + t.Fatal(err) + } + rec := httptest.NewRecorder() + apiRouter.ServeHTTP(rec, req) + after, gerr := obj.GetObjectInfo(t.Context(), bucketName, zero, ObjectOptions{}) + if gerr != nil { + t.Fatal(gerr) + } + t.Logf("%s: zero-byte metadata CopyObject -> %d; stored SSE-C=%v size=%d tags=%q", + instanceType, rec.Code, crypto.SSEC.IsEncrypted(after.UserDefined), after.Size, after.UserTags) + if rec.Code != http.StatusOK { + t.Fatalf("%s: zero-byte metadata CopyObject status %d: %s", instanceType, rec.Code, rec.Body.String()) + } + }) + + t.Run("metadata-copy-is-what-resync-runs", func(t *testing.T) { + // Exactly what replicateAll runs at cmd/bucket-replication.go:1582 once + // the keyless HEAD has been misread: a same bucket, same key CopyObject + // built from getCopyObjMetadata plus the replication marker, carrying no + // customer key. getCopyObjMetadata already sets REPLICA status and + // x-amz-tagging-directive: REPLACE, so the request is replica trusted. + gr, err := obj.GetObjectNInfo(t.Context(), bucketName, object, nil, http.Header{}, ObjectOptions{ + ReplicationRequest: true, + }) + if err != nil { + t.Fatal(err) + } + srcInfo := gr.ObjInfo + gr.Close() + + // minio-go addresses the source version on the copy source and in the + // destination query (api-compose-object.go:307,314). + copySrc := url.QueryEscape(SlashSeparator+bucketName+SlashSeparator+object) + "?versionId=" + srcInfo.VersionID + headers := map[string]string{ + xhttp.AmzCopySource: copySrc, + xhttp.MinIOSourceReplicationRequest: "true", + } + copyMeta := getCopyObjMetadata(srcInfo, "") + for k, v := range copyMeta { + // net/http derives the request body length from a literal + // Content-Length header; minio-go relies on req.ContentLength, so + // drop it here to keep the in-process request faithful. + if strings.EqualFold(k, "content-length") { + t.Logf("%s: dropping content-length=%q from the copy metadata", instanceType, v) + continue + } + headers[k] = v + } + t.Logf("%s: copy metadata keys: %v", instanceType, slices.Sorted(maps.Keys(copyMeta))) + copyURL := getCopyObjectURL("", bucketName, object) + "?versionId=" + srcInfo.VersionID + req, err := newTestSignedRequestV4(http.MethodPut, copyURL, 0, nil, + replicator.AccessKey, replicator.SecretKey, headers) + if err != nil { + t.Fatal(err) + } + rec := httptest.NewRecorder() + apiRouter.ServeHTTP(rec, req) + t.Logf("%s: replication metadata CopyObject (version %s) -> %d %s", instanceType, srcInfo.VersionID, rec.Code, + strings.ReplaceAll(rec.Body.String(), "\n", " ")) + if rec.Code != http.StatusBadRequest { + t.Fatalf("%s: metadata CopyObject status %d, want 400", instanceType, rec.Code) + } + if !strings.Contains(rec.Body.String(), "ExcessData") { + t.Fatalf("%s: metadata CopyObject did not fail with ExcessData: %s", instanceType, rec.Body.String()) + } + + // Whatever the status, the object must still read back with the key. + greq, gerr := newTestSignedRequestV4(http.MethodGet, getGetObjectURL("", bucketName, object), 0, nil, + credentials.AccessKey, credentials.SecretKey, sseHeaders) + if gerr != nil { + t.Fatal(gerr) + } + grec := httptest.NewRecorder() + apiRouter.ServeHTTP(grec, greq) + if grec.Code != http.StatusOK || !bytes.Equal(grec.Body.Bytes(), data) { + t.Errorf("%s: after the replication metadata CopyObject the object no longer reads back: %d (%d bytes)", + instanceType, grec.Code, grec.Body.Len()) + } else { + t.Logf("%s: object still reads back correctly with the customer key", instanceType) + } + }) +} + +// TestAPISSECReplicaRetransmitOverExistingVersion asserts that a full +// retransmit of an SSE-C object reaches the destination when the replica +// already exists with the source version and ETag. checkPreconditionsPUT used +// to reject a matching PreserveETag plus VersionID with 412 for the multipart +// path (the single-part sealed ETag is truncated before the comparison), and +// the sender turns 412 into success, so no part was ever sent and an SSE-C +// replica could never be repaired or updated. See pgsty/silo#120. +func TestAPISSECReplicaRetransmitOverExistingVersion(t *testing.T) { + defer DetectTestLeak(t)() + ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{ + t: t, + objAPITest: testAPISSECReplicaRetransmitOverExistingVersion, + makeBucketOptions: MakeBucketOptions{VersioningEnabled: true}, + }) +} + +func testAPISSECReplicaRetransmitOverExistingVersion(obj ObjectLayer, instanceType, bucketName string, + apiRouter http.Handler, credentials auth.Credentials, t *testing.T, +) { + previousTLS := globalIsTLS + globalIsTLS = true + defer func() { globalIsTLS = previousTLS }() + + key := bytes.Repeat([]byte{0x43}, 32) + keyMD5 := md5.Sum(key) + sseHeaders := map[string]string{ + xhttp.AmzServerSideEncryptionCustomerAlgorithm: xhttp.AmzEncryptionAES, + xhttp.AmzServerSideEncryptionCustomerKey: base64.StdEncoding.EncodeToString(key), + xhttp.AmzServerSideEncryptionCustomerKeyMD5: base64.StdEncoding.EncodeToString(keyMD5[:]), + } + replicator := newObjectAttributesAuthzUser(t, instanceType, bucketName, + `"s3:GetObject","s3:PutObject","s3:ReplicateObject"`) + + replicaHeaders := func(t *testing.T, oi ObjectInfo) map[string]string { + t.Helper() + opts, _, err := putReplicationOpts(t.Context(), "", oi) + if err != nil { + t.Fatal(err) + } + opts.Internal.SourceMTime = time.Time{} + out := make(map[string]string) + for name, values := range opts.Header() { + if len(values) > 0 { + out[name] = values[0] + } + } + out[xhttp.MinIOSourceReplicationRequest] = "true" + out[xhttp.AmzBucketReplicationStatus] = "REPLICA" + out[xhttp.MinIOSourceETag] = oi.ETag + return out + } + + t.Run("single-part-replica-put", func(t *testing.T) { + data := bytes.Repeat([]byte("single-part-ssec-replica-"), 400) + object := "ssec-duplicate/single" + putCopyChecksumSource(t, apiRouter, credentials, bucketName, object, data, sseHeaders) + + gr, err := obj.GetObjectNInfo(t.Context(), bucketName, object, nil, http.Header{}, ObjectOptions{ + ReplicationRequest: true, + }) + if err != nil { + t.Fatal(err) + } + srcInfo := gr.ObjInfo + cipher, err := io.ReadAll(gr) + gr.Close() + if err != nil { + t.Fatal(err) + } + // The source changed a tag after the replica was written: the + // retransmit must carry it onto the same version. + srcInfo.UserTags = "retransmit=single" + hdrs := replicaHeaders(t, srcInfo) + putURL := getPutObjectURL("", bucketName, object) + "?versionId=" + srcInfo.VersionID + req, err := newTestSignedRequestV4(http.MethodPut, putURL, int64(len(cipher)), + bytes.NewReader(cipher), replicator.AccessKey, replicator.SecretKey, hdrs) + if err != nil { + t.Fatal(err) + } + rec := httptest.NewRecorder() + apiRouter.ServeHTTP(rec, req) + if rec.Code != http.StatusOK { + t.Fatalf("%s: single-part replica PUT status %d, want 200: %s", instanceType, rec.Code, rec.Body.String()) + } + assertRetransmittedVersion(t, obj, apiRouter, credentials, bucketName, object, srcInfo.VersionID, "retransmit=single", data, sseHeaders) + }) + + t.Run("zero-byte-replica-put", func(t *testing.T) { + object := "ssec-duplicate/zero" + putCopyChecksumSource(t, apiRouter, credentials, bucketName, object, nil, sseHeaders) + + gr, err := obj.GetObjectNInfo(t.Context(), bucketName, object, nil, http.Header{}, ObjectOptions{ + ReplicationRequest: true, + }) + if err != nil { + t.Fatal(err) + } + srcInfo := gr.ObjInfo + cipher, err := io.ReadAll(gr) + gr.Close() + if err != nil { + t.Fatal(err) + } + srcInfo.UserTags = "retransmit=zero" + hdrs := replicaHeaders(t, srcInfo) + putURL := getPutObjectURL("", bucketName, object) + "?versionId=" + srcInfo.VersionID + req, err := newTestSignedRequestV4(http.MethodPut, putURL, int64(len(cipher)), + bytes.NewReader(cipher), replicator.AccessKey, replicator.SecretKey, hdrs) + if err != nil { + t.Fatal(err) + } + rec := httptest.NewRecorder() + apiRouter.ServeHTTP(rec, req) + if rec.Code != http.StatusOK { + t.Fatalf("%s: zero-byte replica PUT status %d, want 200: %s", instanceType, rec.Code, rec.Body.String()) + } + assertRetransmittedVersion(t, obj, apiRouter, credentials, bucketName, object, srcInfo.VersionID, "retransmit=zero", nil, sseHeaders) + }) + + t.Run("multipart-replica-newmpu", func(t *testing.T) { + data := bytes.Repeat([]byte("multipart-ssec-replica-"), 4096) + object := "ssec-duplicate/multipart" + + newReq, err := newTestSignedRequestV4(http.MethodPost, getNewMultipartURL("", bucketName, object), + 0, nil, credentials.AccessKey, credentials.SecretKey, sseHeaders) + if err != nil { + t.Fatal(err) + } + newRec := httptest.NewRecorder() + apiRouter.ServeHTTP(newRec, newReq) + if newRec.Code != http.StatusOK { + t.Fatalf("%s: source NewMultipart %d: %s", instanceType, newRec.Code, newRec.Body.String()) + } + var srcInit InitiateMultipartUploadResponse + if err = xmlDecoder(newRec.Body, &srcInit, int64(newRec.Body.Len())); err != nil { + t.Fatal(err) + } + partReq, err := newTestSignedRequestV4(http.MethodPut, + getPutObjectPartURL("", bucketName, object, srcInit.UploadID, "1"), int64(len(data)), + bytes.NewReader(data), credentials.AccessKey, credentials.SecretKey, sseHeaders) + if err != nil { + t.Fatal(err) + } + partRec := httptest.NewRecorder() + apiRouter.ServeHTTP(partRec, partReq) + if partRec.Code != http.StatusOK { + t.Fatalf("%s: source PutPart %d: %s", instanceType, partRec.Code, partRec.Body.String()) + } + body, err := xml.Marshal(CompleteMultipartUpload{Parts: []CompletePart{ + {PartNumber: 1, ETag: canonicalizeETag(partRec.Header()[xhttp.ETag][0])}, + }}) + if err != nil { + t.Fatal(err) + } + completeReq, err := newTestSignedRequestV4(http.MethodPost, + getCompleteMultipartUploadURL("", bucketName, object, srcInit.UploadID), int64(len(body)), + bytes.NewReader(body), credentials.AccessKey, credentials.SecretKey, sseHeaders) + if err != nil { + t.Fatal(err) + } + completeRec := httptest.NewRecorder() + apiRouter.ServeHTTP(completeRec, completeReq) + if completeRec.Code != http.StatusOK { + t.Fatalf("%s: source Complete %d: %s", instanceType, completeRec.Code, completeRec.Body.String()) + } + + gr, err := obj.GetObjectNInfo(t.Context(), bucketName, object, nil, http.Header{}, ObjectOptions{ + ReplicationRequest: true, + }) + if err != nil { + t.Fatal(err) + } + srcInfo := gr.ObjInfo + gr.Close() + + srcInfo.UserTags = "retransmit=multipart" + hdrs := replicaHeaders(t, srcInfo) + mpuURL := getNewMultipartURL("", bucketName, object) + "&versionId=" + srcInfo.VersionID + req, err := newTestSignedRequestV4(http.MethodPost, mpuURL, 0, nil, + replicator.AccessKey, replicator.SecretKey, hdrs) + if err != nil { + t.Fatal(err) + } + rec := httptest.NewRecorder() + apiRouter.ServeHTTP(rec, req) + t.Logf("%s: multipart replica NewMultipartUpload over the same version+ETag -> %d %s (source ETag %q, multipart=%v)", + instanceType, rec.Code, strings.ReplaceAll(rec.Body.String(), "\n", " "), + srcInfo.ETag, crypto.IsMultiPart(srcInfo.UserDefined)) + if rec.Code == http.StatusPreconditionFailed { + t.Fatalf("%s: multipart replica upload short-circuited with 412; the sender turns that into "+ + "success, so no part is ever sent", instanceType) + } + if rec.Code != http.StatusOK { + t.Fatalf("%s: replica NewMultipart status %d: %s", instanceType, rec.Code, rec.Body.String()) + } + + // Finish the replica upload the way the sender does and prove the object + // is still readable with the customer key afterwards. + gr2, err := obj.GetObjectNInfo(t.Context(), bucketName, object, nil, http.Header{}, ObjectOptions{ + ReplicationRequest: true, + }) + if err != nil { + t.Fatal(err) + } + rawPart, err := io.ReadAll(gr2) + gr2.Close() + if err != nil { + t.Fatal(err) + } + var replicaInit InitiateMultipartUploadResponse + if err = xmlDecoder(rec.Body, &replicaInit, int64(rec.Body.Len())); err != nil { + t.Fatal(err) + } + partReq2, err := newTestSignedRequestV4(http.MethodPut, + getPutObjectPartURL("", bucketName, object, replicaInit.UploadID, "1"), int64(len(rawPart)), + bytes.NewReader(rawPart), replicator.AccessKey, replicator.SecretKey, + map[string]string{xhttp.MinIOSourceReplicationRequest: "true"}) + if err != nil { + t.Fatal(err) + } + partRec2 := httptest.NewRecorder() + apiRouter.ServeHTTP(partRec2, partReq2) + if partRec2.Code != http.StatusOK { + t.Fatalf("%s: replica PutPart %d: %s", instanceType, partRec2.Code, partRec2.Body.String()) + } + actualSize, err := srcInfo.GetActualSize() + if err != nil { + t.Fatal(err) + } + completeBody2, err := xml.Marshal(CompleteMultipartUpload{Parts: []CompletePart{ + {PartNumber: 1, ETag: canonicalizeETag(partRec2.Header()[xhttp.ETag][0])}, + }}) + if err != nil { + t.Fatal(err) + } + completeReq2, err := newTestSignedRequestV4(http.MethodPost, + getCompleteMultipartUploadURL("", bucketName, object, replicaInit.UploadID), int64(len(completeBody2)), + bytes.NewReader(completeBody2), replicator.AccessKey, replicator.SecretKey, map[string]string{ + xhttp.MinIOSourceReplicationRequest: "true", + xhttp.MinIOSourceMTime: srcInfo.ModTime.Format(time.RFC3339Nano), + xhttp.MinIOSourceETag: srcInfo.ETag, + xhttp.MinIOReplicationActualObjectSize: strconv.FormatInt(actualSize, 10), + }) + if err != nil { + t.Fatal(err) + } + completeRec2 := httptest.NewRecorder() + apiRouter.ServeHTTP(completeRec2, completeReq2) + if completeRec2.Code != http.StatusOK { + t.Fatalf("%s: replica Complete %d: %s", instanceType, completeRec2.Code, completeRec2.Body.String()) + } + assertRetransmittedVersion(t, obj, apiRouter, credentials, bucketName, object, srcInfo.VersionID, "retransmit=multipart", data, sseHeaders) + }) + + t.Run("repairs-an-undecodable-existing-version", func(t *testing.T) { + // A pre-fix destination (issue #109) could persist an SSE-C replica as + // compress(ciphertext) or a re-encrypted body, leaving a stored length + // that is not a valid encryption stream. Resync repairs such a version by + // retransmitting the source's raw ciphertext, but the write must not first + // require the stored, damaged object to decrypt. See issue #120. + data := bytes.Repeat([]byte("SILO raw SSE-C recovery\n"), 400) + object := "ssec-duplicate/undecodable" + putCopyChecksumSource(t, apiRouter, credentials, bucketName, object, data, sseHeaders) + + gr, err := obj.GetObjectNInfo(t.Context(), bucketName, object, nil, http.Header{}, ObjectOptions{ + ReplicationRequest: true, + }) + if err != nil { + t.Fatal(err) + } + srcInfo := gr.ObjInfo + cipher, err := io.ReadAll(gr) + gr.Close() + if err != nil { + t.Fatal(err) + } + + // Stage the damage: overwrite the version with a body too short to be a + // valid encryption stream, standing in for the compression/re-encryption + // an old destination left behind. The staging write itself is a raw + // replica over the still-valid version, so it stores verbatim. + damaged := []byte("dmg!!") + if _, derr := sio.DecryptedSize(uint64(len(damaged))); derr == nil { + t.Fatalf("%s: fixture body of %d bytes is a valid stream length, not undecodable", instanceType, len(damaged)) + } + stageHdrs := replicaHeaders(t, srcInfo) + stageURL := getPutObjectURL("", bucketName, object) + "?versionId=" + srcInfo.VersionID + stageReq, err := newTestSignedRequestV4(http.MethodPut, stageURL, int64(len(damaged)), + bytes.NewReader(damaged), replicator.AccessKey, replicator.SecretKey, stageHdrs) + if err != nil { + t.Fatal(err) + } + stageRec := httptest.NewRecorder() + apiRouter.ServeHTTP(stageRec, stageReq) + if stageRec.Code != http.StatusOK { + t.Fatalf("%s: could not stage the damaged replica: %d %s", instanceType, stageRec.Code, stageRec.Body.String()) + } + // The staged version is genuinely undecodable at the object layer, which + // is exactly what makes DecryptObjectInfo fail during the overwrite. + staged, err := obj.GetObjectInfo(t.Context(), bucketName, object, ObjectOptions{VersionID: srcInfo.VersionID}) + if err != nil { + t.Fatal(err) + } + if _, derr := staged.DecryptedSize(); derr == nil { + t.Fatalf("%s: staged replica is decodable, cannot exercise the repair path", instanceType) + } + + // Retransmit the correct ciphertext over the same version. Before the + // raw-replica precondition exemption this failed with XMinioObjectTampered + // because the damaged object could not decrypt; it must now repair. + srcInfo.UserTags = "retransmit=undecodable" + hdrs := replicaHeaders(t, srcInfo) + putURL := getPutObjectURL("", bucketName, object) + "?versionId=" + srcInfo.VersionID + req, err := newTestSignedRequestV4(http.MethodPut, putURL, int64(len(cipher)), + bytes.NewReader(cipher), replicator.AccessKey, replicator.SecretKey, hdrs) + if err != nil { + t.Fatal(err) + } + rec := httptest.NewRecorder() + apiRouter.ServeHTTP(rec, req) + if rec.Code != http.StatusOK { + t.Fatalf("%s: retransmit over an undecodable version status %d, want 200: %s", instanceType, rec.Code, rec.Body.String()) + } + assertRetransmittedVersion(t, obj, apiRouter, credentials, bucketName, object, srcInfo.VersionID, "retransmit=undecodable", data, sseHeaders) + }) + + // setupHealthySSECVersion writes a normal SSE-C object and returns its + // ObjectInfo (for building replica headers), its ciphertext, and the + // client-visible ETag a keyed reader sees -- the decrypted ETag, which is + // distinct from the stored sealed ETag. + setupHealthySSECVersion := func(t *testing.T, object string, data []byte) (srcInfo ObjectInfo, cipher []byte, clientETag string) { + t.Helper() + putCopyChecksumSource(t, apiRouter, credentials, bucketName, object, data, sseHeaders) + gr, err := obj.GetObjectNInfo(t.Context(), bucketName, object, nil, http.Header{}, ObjectOptions{ReplicationRequest: true}) + if err != nil { + t.Fatal(err) + } + srcInfo = gr.ObjInfo + cipher, err = io.ReadAll(gr) + gr.Close() + if err != nil { + t.Fatal(err) + } + // The client-visible ETag is the decrypted one, which the handler derives + // with the customer key through DecryptObjectInfo; compute it the same way. + keyHeader := http.Header{} + for k, v := range sseHeaders { + keyHeader.Set(k, v) + } + clientETag = getDecryptedETag(keyHeader, srcInfo, false) + if clientETag == "" || clientETag == srcInfo.ETag { + t.Fatalf("%s: client ETag %q is not distinct from the sealed ETag %q", instanceType, clientETag, srcInfo.ETag) + } + return srcInfo, cipher, clientETag + } + + // A conditional replica PUT must compare the public precondition against the + // client-visible ETag, not the stored sealed one. Skipping DecryptObjectInfo + // for every raw SSE-C replica (not only a pure overwrite) left oi.ETag sealed + // and inverted both conditions. + t.Run("if-match-on-the-client-etag-proceeds", func(t *testing.T) { + object := "ssec-duplicate/cond-if-match" + srcInfo, cipher, clientETag := setupHealthySSECVersion(t, object, bytes.Repeat([]byte("cond-if-match-"), 64)) + hdrs := replicaHeaders(t, srcInfo) + hdrs[xhttp.IfMatch] = "\"" + clientETag + "\"" + req, err := newTestSignedRequestV4(http.MethodPut, + getPutObjectURL("", bucketName, object)+"?versionId="+srcInfo.VersionID, + int64(len(cipher)), bytes.NewReader(cipher), replicator.AccessKey, replicator.SecretKey, hdrs) + if err != nil { + t.Fatal(err) + } + rec := httptest.NewRecorder() + apiRouter.ServeHTTP(rec, req) + if rec.Code != http.StatusOK { + t.Fatalf("%s: If-Match on the client ETag status %d, want 200: %s", instanceType, rec.Code, rec.Body.String()) + } + }) + + t.Run("if-none-match-on-the-client-etag-fails", func(t *testing.T) { + object := "ssec-duplicate/cond-if-none-match" + srcInfo, cipher, clientETag := setupHealthySSECVersion(t, object, bytes.Repeat([]byte("cond-if-none-"), 64)) + hdrs := replicaHeaders(t, srcInfo) + hdrs[xhttp.IfNoneMatch] = "\"" + clientETag + "\"" + req, err := newTestSignedRequestV4(http.MethodPut, + getPutObjectURL("", bucketName, object)+"?versionId="+srcInfo.VersionID, + int64(len(cipher)), bytes.NewReader(cipher), replicator.AccessKey, replicator.SecretKey, hdrs) + if err != nil { + t.Fatal(err) + } + rec := httptest.NewRecorder() + apiRouter.ServeHTTP(rec, req) + if rec.Code != http.StatusPreconditionFailed { + t.Fatalf("%s: If-None-Match on the client ETag status %d, want 412: %s", instanceType, rec.Code, rec.Body.String()) + } + }) +} + +// assertRetransmittedVersion checks that a retransmit landed on the addressed +// version: the changed tag is stored on it and it still reads back with the +// customer key. +func assertRetransmittedVersion(t *testing.T, obj ObjectLayer, apiRouter http.Handler, credentials auth.Credentials, + bucketName, object, versionID, wantTags string, want []byte, sseHeaders map[string]string, +) { + t.Helper() + info, err := obj.GetObjectInfo(t.Context(), bucketName, object, ObjectOptions{VersionID: versionID}) + if err != nil { + t.Fatalf("version %s after retransmit: %v", versionID, err) + } + if info.UserTags != wantTags { + t.Errorf("version %s tags after retransmit %q, want %q", versionID, info.UserTags, wantTags) + } + if !crypto.SSEC.IsEncrypted(info.UserDefined) { + t.Errorf("version %s lost its SSE-C seal after retransmit", versionID) + } + getReq, err := newTestSignedRequestV4(http.MethodGet, getGetObjectURL("", bucketName, object)+"?versionId="+versionID, + 0, nil, credentials.AccessKey, credentials.SecretKey, sseHeaders) + if err != nil { + t.Fatal(err) + } + getRec := httptest.NewRecorder() + apiRouter.ServeHTTP(getRec, getReq) + if getRec.Code != http.StatusOK || !bytes.Equal(getRec.Body.Bytes(), want) { + t.Fatalf("version %s does not read back with the customer key after retransmit: %d (%d bytes, want %d)", + versionID, getRec.Code, getRec.Body.Len(), len(want)) + } +} + +// TestPutReplicationOptsRetentionRemoval asserts that a source version whose +// retention was removed (stored as an empty mode and date) still builds +// replication options, carrying the removal's ordering timestamp and no value. +func TestPutReplicationOptsRetentionRemoval(t *testing.T) { + removedAt := time.Date(2026, 9, 5, 10, 0, 0, 0, time.UTC) + oi := ObjectInfo{ + Bucket: "b", Name: "o", VersionID: "v1", ModTime: removedAt.Add(-time.Hour), + UserDefined: map[string]string{ + strings.ToLower(xhttp.AmzObjectLockMode): "", + strings.ToLower(xhttp.AmzObjectLockRetainUntilDate): "", + ReservedMetadataPrefixLower + ObjectLockRetentionTimestamp: removedAt.Format(time.RFC3339Nano), + }, + } + opts, _, err := putReplicationOpts(t.Context(), "", oi) + if err != nil { + t.Fatalf("putReplicationOpts on a removed retention: %v", err) + } + if opts.Mode != "" || !opts.RetainUntilDate.IsZero() { + t.Errorf("removal sent as a retention: mode %q date %v", opts.Mode, opts.RetainUntilDate) + } + if !opts.Internal.RetentionTimestamp.Equal(removedAt) { + t.Errorf("removal timestamp %v, want %v", opts.Internal.RetentionTimestamp, removedAt) + } + if hdr := opts.Header(); hdr.Get(xhttp.AmzObjectLockMode) != "" || hdr.Get(xhttp.AmzObjectLockRetainUntilDate) != "" || + hdr.Get(xhttp.MinIOSourceObjectRetentionTimestamp) == "" { + t.Errorf("removal headers %v: want no lock value and a retention timestamp", hdr) + } +} + +// TestAPISSECReplicaWriteExemptionIsKeyedOnTheIncomingWrite pins the predicate +// of the duplicate version and ETag exemption: it applies to an authenticated +// replica write that carries an SSE-C seal, whatever the destination holds, and +// not to a plaintext replica write that happens to match an SSE-C destination +// version. See pgsty/silo#120. +func TestAPISSECReplicaWriteExemptionIsKeyedOnTheIncomingWrite(t *testing.T) { + defer DetectTestLeak(t)() + ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{ + t: t, + objAPITest: testAPISSECReplicaWriteExemptionIsKeyedOnTheIncomingWrite, + makeBucketOptions: MakeBucketOptions{VersioningEnabled: true}, + }) +} + +func testAPISSECReplicaWriteExemptionIsKeyedOnTheIncomingWrite(obj ObjectLayer, instanceType, bucketName string, + apiRouter http.Handler, credentials auth.Credentials, t *testing.T, +) { + previousTLS := globalIsTLS + globalIsTLS = true + defer func() { globalIsTLS = previousTLS }() + + key := bytes.Repeat([]byte{0x44}, 32) + keyMD5 := md5.Sum(key) + sseHeaders := map[string]string{ + xhttp.AmzServerSideEncryptionCustomerAlgorithm: xhttp.AmzEncryptionAES, + xhttp.AmzServerSideEncryptionCustomerKey: base64.StdEncoding.EncodeToString(key), + xhttp.AmzServerSideEncryptionCustomerKeyMD5: base64.StdEncoding.EncodeToString(keyMD5[:]), + } + replicator := newObjectAttributesAuthzUser(t, instanceType, bucketName, + `"s3:GetObject","s3:PutObject","s3:ReplicateObject"`) + + rawOf := func(t *testing.T, object string) (ObjectInfo, []byte) { + t.Helper() + gr, err := obj.GetObjectNInfo(t.Context(), bucketName, object, nil, http.Header{}, ObjectOptions{ + ReplicationRequest: true, + }) + if err != nil { + t.Fatal(err) + } + defer gr.Close() + raw, err := io.ReadAll(gr) + if err != nil { + t.Fatal(err) + } + return gr.ObjInfo, raw + } + replicaHeaders := func(t *testing.T, oi ObjectInfo) map[string]string { + t.Helper() + opts, _, err := putReplicationOpts(t.Context(), "", oi) + if err != nil { + t.Fatal(err) + } + opts.Internal.SourceMTime = time.Time{} + out := make(map[string]string) + for name, values := range opts.Header() { + if len(values) > 0 { + out[name] = values[0] + } + } + out[xhttp.MinIOSourceReplicationRequest] = "true" + out[xhttp.AmzBucketReplicationStatus] = "REPLICA" + out[xhttp.MinIOSourceETag] = oi.ETag + return out + } + replicaPut := func(t *testing.T, object, versionID string, body []byte, hdrs map[string]string) *httptest.ResponseRecorder { + t.Helper() + req, err := newTestSignedRequestV4(http.MethodPut, + getPutObjectURL("", bucketName, object)+"?versionId="+versionID, int64(len(body)), + bytes.NewReader(body), replicator.AccessKey, replicator.SecretKey, hdrs) + if err != nil { + t.Fatal(err) + } + rec := httptest.NewRecorder() + apiRouter.ServeHTTP(rec, req) + return rec + } + + // putSSECMultipart stores a one-part SSE-C multipart object: unlike a + // single-part SSE-C object, whose stored ETag is the sealed one and never + // matches the sender's, a multipart ETag compares equal, which is what + // makes the duplicate version and ETag check reachable at all. + putSSECMultipart := func(t *testing.T, object string, data []byte) { + t.Helper() + newReq, err := newTestSignedRequestV4(http.MethodPost, getNewMultipartURL("", bucketName, object), + 0, nil, credentials.AccessKey, credentials.SecretKey, sseHeaders) + if err != nil { + t.Fatal(err) + } + newRec := httptest.NewRecorder() + apiRouter.ServeHTTP(newRec, newReq) + if newRec.Code != http.StatusOK { + t.Fatalf("%s: NewMultipart %d: %s", instanceType, newRec.Code, newRec.Body.String()) + } + var init InitiateMultipartUploadResponse + if err = xmlDecoder(newRec.Body, &init, int64(newRec.Body.Len())); err != nil { + t.Fatal(err) + } + partReq, err := newTestSignedRequestV4(http.MethodPut, + getPutObjectPartURL("", bucketName, object, init.UploadID, "1"), int64(len(data)), + bytes.NewReader(data), credentials.AccessKey, credentials.SecretKey, sseHeaders) + if err != nil { + t.Fatal(err) + } + partRec := httptest.NewRecorder() + apiRouter.ServeHTTP(partRec, partReq) + if partRec.Code != http.StatusOK { + t.Fatalf("%s: PutPart %d: %s", instanceType, partRec.Code, partRec.Body.String()) + } + body, err := xml.Marshal(CompleteMultipartUpload{Parts: []CompletePart{ + {PartNumber: 1, ETag: canonicalizeETag(partRec.Header()[xhttp.ETag][0])}, + }}) + if err != nil { + t.Fatal(err) + } + completeReq, err := newTestSignedRequestV4(http.MethodPost, + getCompleteMultipartUploadURL("", bucketName, object, init.UploadID), int64(len(body)), + bytes.NewReader(body), credentials.AccessKey, credentials.SecretKey, sseHeaders) + if err != nil { + t.Fatal(err) + } + completeRec := httptest.NewRecorder() + apiRouter.ServeHTTP(completeRec, completeReq) + if completeRec.Code != http.StatusOK { + t.Fatalf("%s: Complete %d: %s", instanceType, completeRec.Code, completeRec.Body.String()) + } + } + + t.Run("plaintext-replica-over-ssec-version-is-still-rejected", func(t *testing.T) { + object := "ssec-exemption/ssec-destination" + putSSECMultipart(t, object, bytes.Repeat([]byte("ssec-destination-"), 4096)) + srcInfo, raw := rawOf(t, object) + hdrs := replicaHeaders(t, srcInfo) + // Without the seal the incoming write is a plaintext replica that merely + // carries the stored version and ETag: the duplicate check still applies. + for name := range hdrs { + if strings.HasPrefix(name, "X-Minio-Replication-Server-Side-Encryption-") { + delete(hdrs, name) + } + } + rec := replicaPut(t, object, srcInfo.VersionID, raw, hdrs) + if rec.Code != http.StatusPreconditionFailed { + t.Fatalf("%s: plaintext replica write over an existing SSE-C version answered %d, want 412: %s", + instanceType, rec.Code, rec.Body.String()) + } + }) + + t.Run("ssec-replica-over-plaintext-version-is-exempted", func(t *testing.T) { + object := "ssec-exemption/plain-destination" + // A plaintext version first, then an SSE-C version of the same key so + // the seal is bound to this object path. + putCopyChecksumSource(t, apiRouter, credentials, bucketName, object, []byte("plaintext destination version"), nil) + plainInfo, err := obj.GetObjectInfo(t.Context(), bucketName, object, ObjectOptions{}) + if err != nil { + t.Fatal(err) + } + data := bytes.Repeat([]byte("ssec-source-"), 400) + putCopyChecksumSource(t, apiRouter, credentials, bucketName, object, data, sseHeaders) + srcInfo, raw := rawOf(t, object) + + // The incoming write carries the SSE-C seal and addresses the plaintext + // version with its ETag: the exemption is decided by the incoming + // write, not by what the destination holds. + hdrs := replicaHeaders(t, srcInfo) + hdrs[xhttp.MinIOSourceETag] = plainInfo.ETag + rec := replicaPut(t, object, plainInfo.VersionID, raw, hdrs) + if rec.Code != http.StatusOK { + t.Fatalf("%s: SSE-C replica write over a matching plaintext version answered %d, want 200: %s", + instanceType, rec.Code, rec.Body.String()) + } + getReq, err := newTestSignedRequestV4(http.MethodGet, + getGetObjectURL("", bucketName, object)+"?versionId="+plainInfo.VersionID, 0, nil, + credentials.AccessKey, credentials.SecretKey, sseHeaders) + if err != nil { + t.Fatal(err) + } + getRec := httptest.NewRecorder() + apiRouter.ServeHTTP(getRec, getReq) + if getRec.Code != http.StatusOK || !bytes.Equal(getRec.Body.Bytes(), data) { + t.Fatalf("%s: the retransmitted version does not read back with the customer key: %d (%d bytes)", + instanceType, getRec.Code, getRec.Body.Len()) + } + }) +} + +// Retain-until values in the millisecond form ISO8601Format round-trips to, so +// a value applied through the handler reads back byte-for-byte. +const ( + retransmitRetainUntilNewer = "2031-01-01T00:00:00.000Z" + retransmitRetainUntilStale = "2028-01-01T00:00:00.000Z" +) + +// TestAPISSECReplicaRetransmitObjectLockOrdering proves that the full SSE-C +// replica retransmit orders the Object Lock update it carries against the state +// already stored on the addressed version, the same way the metadata CopyObject +// path does. Before issue #120 routed these writes through PutObjectHandler the +// handler applied the incoming retention and legal hold directly and never +// persisted the ordering timestamps, so a retransmit carrying an older value +// could overwrite a destination version's newer one. See pgsty/silo#120. +func TestAPISSECReplicaRetransmitObjectLockOrdering(t *testing.T) { + defer DetectTestLeak(t)() + ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{ + t: t, + objAPITest: testAPISSECReplicaRetransmitObjectLockOrdering, + makeBucketOptions: MakeBucketOptions{LockEnabled: true}, + }) +} + +func testAPISSECReplicaRetransmitObjectLockOrdering(obj ObjectLayer, instanceType, bucketName string, + apiRouter http.Handler, credentials auth.Credentials, t *testing.T, +) { + previousTLS := globalIsTLS + globalIsTLS = true + defer func() { globalIsTLS = previousTLS }() + + key := bytes.Repeat([]byte{0x45}, 32) + keyMD5 := md5.Sum(key) + sseHeaders := map[string]string{ + xhttp.AmzServerSideEncryptionCustomerAlgorithm: xhttp.AmzEncryptionAES, + xhttp.AmzServerSideEncryptionCustomerKey: base64.StdEncoding.EncodeToString(key), + xhttp.AmzServerSideEncryptionCustomerKeyMD5: base64.StdEncoding.EncodeToString(keyMD5[:]), + } + + // seed writes a fresh SSE-C source version with no Object Lock state and + // returns its version id. + seed := func(t *testing.T, object string) string { + t.Helper() + putCopyChecksumSource(t, apiRouter, credentials, bucketName, object, + bytes.Repeat([]byte("ssec-lock-ordering-"), 64), sseHeaders) + info, err := obj.GetObjectInfo(t.Context(), bucketName, object, ObjectOptions{}) + if err != nil { + t.Fatal(err) + } + return info.VersionID + } + + // retransmit sends the full SSE-C replica retransmit the sender emits for the + // addressed version, over its raw ciphertext, carrying exactly the given + // Object Lock headers on top of the replica seal. The credential is the admin + // user so the retention and legal-hold permission checks pass; the request is + // still a trusted replica because it carries the marker and REPLICA status. + retransmit := func(t *testing.T, object, versionID string, lockHeaders map[string]string) { + t.Helper() + gr, err := obj.GetObjectNInfo(t.Context(), bucketName, object, nil, http.Header{}, ObjectOptions{ + ReplicationRequest: true, + VersionID: versionID, + }) + if err != nil { + t.Fatal(err) + } + srcInfo := gr.ObjInfo + cipher, err := io.ReadAll(gr) + gr.Close() + if err != nil { + t.Fatal(err) + } + opts, _, err := putReplicationOpts(t.Context(), "", srcInfo) + if err != nil { + t.Fatal(err) + } + opts.Internal.SourceMTime = time.Time{} + hdrs := make(map[string]string) + for name, values := range opts.Header() { + if len(values) > 0 { + hdrs[name] = values[0] + } + } + hdrs[xhttp.MinIOSourceReplicationRequest] = "true" + hdrs[xhttp.AmzBucketReplicationStatus] = "REPLICA" + hdrs[xhttp.MinIOSourceETag] = srcInfo.ETag + // The case owns the lock instruction: drop any lock header the sender + // derived from the source version. + for _, name := range []string{ + xhttp.AmzObjectLockMode, xhttp.AmzObjectLockRetainUntilDate, xhttp.AmzObjectLockLegalHold, + xhttp.MinIOSourceObjectRetentionTimestamp, xhttp.MinIOSourceObjectLegalHoldTimestamp, + } { + delete(hdrs, name) + } + maps.Copy(hdrs, lockHeaders) + + req, err := newTestSignedRequestV4(http.MethodPut, + getPutObjectURL("", bucketName, object)+"?versionId="+versionID, int64(len(cipher)), + bytes.NewReader(cipher), credentials.AccessKey, credentials.SecretKey, hdrs) + if err != nil { + t.Fatal(err) + } + rec := httptest.NewRecorder() + apiRouter.ServeHTTP(rec, req) + if rec.Code != http.StatusOK { + t.Fatalf("%s: replica retransmit PUT status %d, want 200: %s", instanceType, rec.Code, rec.Body.String()) + } + } + + t.Run("older-legal-hold-off-does-not-clear-newer-on", func(t *testing.T) { + object := "ssec-lock-ordering/legal-hold" + versionID := seed(t, object) + + // Establish the newer legal hold ON. Its timestamp persistence is proven + // by the retention sibling test, so here only require the value took + // effect before the older OFF arrives, so the clobber below is what tells + // a fixed handler from a broken one. + retransmit(t, object, versionID, map[string]string{ + xhttp.AmzObjectLockLegalHold: "ON", + xhttp.MinIOSourceObjectLegalHoldTimestamp: objectLockTestStamp1000, + }) + if got := readObjectLockFields(t, obj, bucketName, object, versionID); got.legalHold != "ON" { + t.Fatalf("%s: seeding legal hold ON failed: got %+v", instanceType, got) + } + + // A retransmit carrying an older legal-hold OFF must not clear it. + retransmit(t, object, versionID, map[string]string{ + xhttp.AmzObjectLockLegalHold: "OFF", + xhttp.MinIOSourceObjectLegalHoldTimestamp: objectLockTestStamp0900, + }) + want := objectLockFields{legalHold: "ON", legalHoldStamp: objectLockTestStamp1000} + if got := readObjectLockFields(t, obj, bucketName, object, versionID); got != want { + t.Fatalf("%s: an older legal-hold OFF cleared the newer ON: got %+v, want %+v", instanceType, got, want) + } + }) + + t.Run("newer-retention-applies-and-persists-its-timestamp", func(t *testing.T) { + object := "ssec-lock-ordering/retention-applies" + versionID := seed(t, object) + + retransmit(t, object, versionID, map[string]string{ + xhttp.AmzObjectLockMode: "GOVERNANCE", + xhttp.AmzObjectLockRetainUntilDate: retransmitRetainUntilNewer, + xhttp.MinIOSourceObjectRetentionTimestamp: objectLockTestStamp1000, + }) + // The value applies and, crucially, the ordering timestamp is persisted so + // a later stale update can be recognized as older. + want := objectLockFields{ + mode: "GOVERNANCE", retainUntil: retransmitRetainUntilNewer, retentionStamp: objectLockTestStamp1000, + } + if got := readObjectLockFields(t, obj, bucketName, object, versionID); got != want { + t.Fatalf("%s: newer retention did not apply and persist its timestamp: got %+v, want %+v", instanceType, got, want) + } + }) + + t.Run("stale-retention-update-is-ignored", func(t *testing.T) { + object := "ssec-lock-ordering/retention-stale" + versionID := seed(t, object) + + // Establish the newer retention first; its timestamp persistence is proven + // by the sibling test above, so here only require the value took effect, so + // the stale overwrite below is what tells a fixed handler from a broken one. + retransmit(t, object, versionID, map[string]string{ + xhttp.AmzObjectLockMode: "GOVERNANCE", + xhttp.AmzObjectLockRetainUntilDate: retransmitRetainUntilNewer, + xhttp.MinIOSourceObjectRetentionTimestamp: objectLockTestStamp1000, + }) + if got := readObjectLockFields(t, obj, bucketName, object, versionID); got.mode != "GOVERNANCE" || got.retainUntil != retransmitRetainUntilNewer { + t.Fatalf("%s: seeding the newer retention failed: got %+v", instanceType, got) + } + + // A retransmit carrying an older retention with a different date must be + // ignored; the newer date and its ordering timestamp survive. + retransmit(t, object, versionID, map[string]string{ + xhttp.AmzObjectLockMode: "GOVERNANCE", + xhttp.AmzObjectLockRetainUntilDate: retransmitRetainUntilStale, + xhttp.MinIOSourceObjectRetentionTimestamp: objectLockTestStamp0900, + }) + want := objectLockFields{ + mode: "GOVERNANCE", retainUntil: retransmitRetainUntilNewer, retentionStamp: objectLockTestStamp1000, + } + if got := readObjectLockFields(t, obj, bucketName, object, versionID); got != want { + t.Fatalf("%s: a stale retention update overwrote the newer one: got %+v, want %+v", instanceType, got, want) + } + }) +} + +// TestAPISSECReplicaRetransmitMultipartObjectLockOrdering proves the ordering +// fix also covers the multipart initiation path #120 routes an SSE-C replica +// through: NewMultipartUploadHandler reads the addressed version's stored lock +// state and orders the incoming update against it, so a large-object retransmit +// carrying an older legal-hold OFF cannot clear a newer ON. See pgsty/silo#120. +func TestAPISSECReplicaRetransmitMultipartObjectLockOrdering(t *testing.T) { + defer DetectTestLeak(t)() + ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{ + t: t, + objAPITest: testAPISSECReplicaRetransmitMultipartObjectLockOrdering, + makeBucketOptions: MakeBucketOptions{LockEnabled: true}, + }) +} + +func testAPISSECReplicaRetransmitMultipartObjectLockOrdering(obj ObjectLayer, instanceType, bucketName string, + apiRouter http.Handler, credentials auth.Credentials, t *testing.T, +) { + previousTLS := globalIsTLS + globalIsTLS = true + defer func() { globalIsTLS = previousTLS }() + + key := bytes.Repeat([]byte{0x46}, 32) + keyMD5 := md5.Sum(key) + sseHeaders := map[string]string{ + xhttp.AmzServerSideEncryptionCustomerAlgorithm: xhttp.AmzEncryptionAES, + xhttp.AmzServerSideEncryptionCustomerKey: base64.StdEncoding.EncodeToString(key), + xhttp.AmzServerSideEncryptionCustomerKeyMD5: base64.StdEncoding.EncodeToString(keyMD5[:]), + } + + // replicaSealHeaders returns the sender's replica seal and marker headers for + // the addressed version, with every Object Lock header stripped so the case + // owns the lock instruction. + replicaSealHeaders := func(t *testing.T, srcInfo ObjectInfo) map[string]string { + t.Helper() + opts, _, err := putReplicationOpts(t.Context(), "", srcInfo) + if err != nil { + t.Fatal(err) + } + opts.Internal.SourceMTime = time.Time{} + hdrs := make(map[string]string) + for name, values := range opts.Header() { + if len(values) > 0 { + hdrs[name] = values[0] + } + } + hdrs[xhttp.MinIOSourceReplicationRequest] = "true" + hdrs[xhttp.AmzBucketReplicationStatus] = "REPLICA" + hdrs[xhttp.MinIOSourceETag] = srcInfo.ETag + for _, name := range []string{ + xhttp.AmzObjectLockMode, xhttp.AmzObjectLockRetainUntilDate, xhttp.AmzObjectLockLegalHold, + xhttp.MinIOSourceObjectRetentionTimestamp, xhttp.MinIOSourceObjectLegalHoldTimestamp, + } { + delete(hdrs, name) + } + return hdrs + } + + object := "ssec-lock-ordering/multipart" + // A single-part SSE-C source is enough; the retransmit re-uploads its bytes + // as one multipart part and commits the addressed version. + putCopyChecksumSource(t, apiRouter, credentials, bucketName, object, + bytes.Repeat([]byte("ssec-multipart-lock-ordering-"), 64), sseHeaders) + base, err := obj.GetObjectInfo(t.Context(), bucketName, object, ObjectOptions{}) + if err != nil { + t.Fatal(err) + } + versionID := base.VersionID + + // Establish the newer legal hold ON through the single-part PUT retransmit. + { + gr, err := obj.GetObjectNInfo(t.Context(), bucketName, object, nil, http.Header{}, ObjectOptions{ + ReplicationRequest: true, VersionID: versionID, + }) + if err != nil { + t.Fatal(err) + } + srcInfo := gr.ObjInfo + cipher, rerr := io.ReadAll(gr) + gr.Close() + if rerr != nil { + t.Fatal(rerr) + } + hdrs := replicaSealHeaders(t, srcInfo) + hdrs[xhttp.AmzObjectLockLegalHold] = "ON" + hdrs[xhttp.MinIOSourceObjectLegalHoldTimestamp] = objectLockTestStamp1000 + req, rerr := newTestSignedRequestV4(http.MethodPut, + getPutObjectURL("", bucketName, object)+"?versionId="+versionID, int64(len(cipher)), + bytes.NewReader(cipher), credentials.AccessKey, credentials.SecretKey, hdrs) + if rerr != nil { + t.Fatal(rerr) + } + rec := httptest.NewRecorder() + apiRouter.ServeHTTP(rec, req) + if rec.Code != http.StatusOK { + t.Fatalf("%s: seeding legal hold ON via PUT status %d: %s", instanceType, rec.Code, rec.Body.String()) + } + if got := readObjectLockFields(t, obj, bucketName, object, versionID); got.legalHold != "ON" { + t.Fatalf("%s: seeding legal hold ON failed: got %+v", instanceType, got) + } + } + + // A multipart retransmit carrying an older legal-hold OFF must not clear it. + gr, err := obj.GetObjectNInfo(t.Context(), bucketName, object, nil, http.Header{}, ObjectOptions{ + ReplicationRequest: true, VersionID: versionID, + }) + if err != nil { + t.Fatal(err) + } + srcInfo := gr.ObjInfo + cipher, err := io.ReadAll(gr) + gr.Close() + if err != nil { + t.Fatal(err) + } + actualSize, err := srcInfo.GetActualSize() + if err != nil { + t.Fatal(err) + } + + newHdrs := replicaSealHeaders(t, srcInfo) + newHdrs[xhttp.AmzObjectLockLegalHold] = "OFF" + newHdrs[xhttp.MinIOSourceObjectLegalHoldTimestamp] = objectLockTestStamp0900 + newReq, err := newTestSignedRequestV4(http.MethodPost, + getNewMultipartURL("", bucketName, object)+"&versionId="+versionID, 0, nil, + credentials.AccessKey, credentials.SecretKey, newHdrs) + if err != nil { + t.Fatal(err) + } + newRec := httptest.NewRecorder() + apiRouter.ServeHTTP(newRec, newReq) + if newRec.Code != http.StatusOK { + t.Fatalf("%s: replica NewMultipartUpload status %d: %s", instanceType, newRec.Code, newRec.Body.String()) + } + var init InitiateMultipartUploadResponse + if err = xmlDecoder(newRec.Body, &init, int64(newRec.Body.Len())); err != nil { + t.Fatal(err) + } + + partReq, err := newTestSignedRequestV4(http.MethodPut, + getPutObjectPartURL("", bucketName, object, init.UploadID, "1"), int64(len(cipher)), + bytes.NewReader(cipher), credentials.AccessKey, credentials.SecretKey, + map[string]string{xhttp.MinIOSourceReplicationRequest: "true"}) + if err != nil { + t.Fatal(err) + } + partRec := httptest.NewRecorder() + apiRouter.ServeHTTP(partRec, partReq) + if partRec.Code != http.StatusOK { + t.Fatalf("%s: replica PutObjectPart status %d: %s", instanceType, partRec.Code, partRec.Body.String()) + } + + completeBody, err := xml.Marshal(CompleteMultipartUpload{Parts: []CompletePart{ + {PartNumber: 1, ETag: canonicalizeETag(partRec.Header()[xhttp.ETag][0])}, + }}) + if err != nil { + t.Fatal(err) + } + completeReq, err := newTestSignedRequestV4(http.MethodPost, + getCompleteMultipartUploadURL("", bucketName, object, init.UploadID), int64(len(completeBody)), + bytes.NewReader(completeBody), credentials.AccessKey, credentials.SecretKey, map[string]string{ + xhttp.MinIOSourceReplicationRequest: "true", + xhttp.MinIOSourceMTime: srcInfo.ModTime.Format(time.RFC3339Nano), + xhttp.MinIOSourceETag: srcInfo.ETag, + xhttp.MinIOReplicationActualObjectSize: strconv.FormatInt(actualSize, 10), + }) + if err != nil { + t.Fatal(err) + } + completeRec := httptest.NewRecorder() + apiRouter.ServeHTTP(completeRec, completeReq) + if completeRec.Code != http.StatusOK { + t.Fatalf("%s: replica CompleteMultipartUpload status %d: %s", instanceType, completeRec.Code, completeRec.Body.String()) + } + + want := objectLockFields{legalHold: "ON", legalHoldStamp: objectLockTestStamp1000} + if got := readObjectLockFields(t, obj, bucketName, object, versionID); got != want { + t.Fatalf("%s: an older legal-hold OFF via multipart cleared the newer ON: got %+v, want %+v", instanceType, got, want) + } +} + +// TestReplicaStoredLock verifies how a replica write reads the destination +// version's stored lock state before ordering its update: a present version +// yields its state, a missing object or version yields an empty state so a first +// write is not blocked, and any other read error (a quorum loss, a timeout) is +// returned so the caller fails the write instead of ordering an incoming update +// against lock state it merely could not read. See pgsty/silo#120. +func TestReplicaStoredLock(t *testing.T) { + fixed := func(oi ObjectInfo, err error) GetObjectInfoFn { + return func(context.Context, string, string, ObjectOptions) (ObjectInfo, error) { return oi, err } + } + stored := ObjectInfo{UserDefined: map[string]string{ + strings.ToLower(xhttp.AmzObjectLockLegalHold): "ON", + ReservedMetadataPrefixLower + ObjectLockLegalHoldTimestamp: objectLockTestStamp1000, + }} + + t.Run("present-version-returns-its-state", func(t *testing.T) { + got, err := replicaStoredLock(context.Background(), fixed(stored, nil), "b", "o", "v") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if got.legalHold != "ON" || got.legalHoldTimestamp != objectLockTestStamp1000 { + t.Fatalf("stored lock state = %+v, want legal hold ON stamped %q", got, objectLockTestStamp1000) + } + }) + + for name, notFound := range map[string]error{ + "object-not-found": ObjectNotFound{Bucket: "b", Object: "o"}, + "version-not-found": VersionNotFound{Bucket: "b", Object: "o", VersionID: "v"}, + } { + t.Run(name+"-is-empty-state", func(t *testing.T) { + got, err := replicaStoredLock(context.Background(), fixed(ObjectInfo{}, notFound), "b", "o", "v") + if err != nil { + t.Fatalf("a not-found read must not error: %v", err) + } + if got != (objectLockState{}) { + t.Fatalf("a not-found read must yield empty state, got %+v", got) + } + }) + } + + t.Run("transient-read-error-propagates", func(t *testing.T) { + boom := InsufficientReadQuorum{} + got, err := replicaStoredLock(context.Background(), fixed(ObjectInfo{}, boom), "b", "o", "v") + if err == nil { + t.Fatal("a transient read error must propagate, not be treated as absent lock state") + } + if isErrObjectNotFound(err) || isErrVersionNotFound(err) { + t.Fatalf("transient error misclassified as not-found: %v", err) + } + if got != (objectLockState{}) { + t.Fatalf("on a read error the caller must get empty state and fail the write, got %+v", got) + } + }) +} + +// TestPutReplicationOptsRetentionRemovalTimestampOnly asserts that a version +// whose retention was removed on the retransmit PUT path -- stored as an +// ordering timestamp with the value keys absent, not empty -- still builds +// replication options that carry the removal timestamp, so the next hop can +// order the removal instead of keeping obsolete retention. See pgsty/silo#120. +func TestPutReplicationOptsRetentionRemovalTimestampOnly(t *testing.T) { + removedAt := time.Date(2026, 9, 5, 10, 0, 0, 0, time.UTC) + oi := ObjectInfo{ + Bucket: "b", Name: "o", VersionID: "v1", ModTime: removedAt.Add(-time.Hour), + UserDefined: map[string]string{ + // Only the reserved ordering timestamp; no mode/date keys at all. + ReservedMetadataPrefixLower + ObjectLockRetentionTimestamp: removedAt.Format(time.RFC3339Nano), + }, + } + opts, _, err := putReplicationOpts(t.Context(), "", oi) + if err != nil { + t.Fatalf("putReplicationOpts on a timestamp-only removal: %v", err) + } + if opts.Mode != "" || !opts.RetainUntilDate.IsZero() { + t.Errorf("removal sent as a retention: mode %q date %v", opts.Mode, opts.RetainUntilDate) + } + if !opts.Internal.RetentionTimestamp.Equal(removedAt) { + t.Errorf("removal timestamp %v, want %v", opts.Internal.RetentionTimestamp, removedAt) + } + if hdr := opts.Header(); hdr.Get(xhttp.AmzObjectLockMode) != "" || hdr.Get(xhttp.AmzObjectLockRetainUntilDate) != "" || + hdr.Get(xhttp.MinIOSourceObjectRetentionTimestamp) == "" { + t.Errorf("removal headers %v: want no lock value and a retention timestamp", hdr) + } +} + +// TestAPIReplicaMarkerOnlyAppliesObjectLock guards the regression the shared +// ordering helper could introduce: a trusted peer write that carries the +// internal replication marker but NOT REPLICA status (replicationRequest true, +// replicaTrusted false) must keep ordinary write semantics and apply its +// validated Object Lock, not restore an empty stored state. It covers an +// explicit legal hold and a bucket-default retention, on both the PUT and the +// multipart-initiation paths. See pgsty/silo#120 (Codex finding 3). +func TestAPIReplicaMarkerOnlyAppliesObjectLock(t *testing.T) { + defer DetectTestLeak(t)() + ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{ + t: t, + objAPITest: testAPIReplicaMarkerOnlyAppliesObjectLock, + makeBucketOptions: MakeBucketOptions{LockEnabled: true}, + }) +} + +func testAPIReplicaMarkerOnlyAppliesObjectLock(obj ObjectLayer, instanceType, bucketName string, + apiRouter http.Handler, credentials auth.Credentials, t *testing.T, +) { + // A trusted peer credential holding ReplicateObject plus the lock permissions + // checkPutObjectLockAllowed enforces even for a marker-only write. + peer := newObjectAttributesAuthzUser(t, instanceType, bucketName, + `"s3:GetObject","s3:PutObject","s3:ReplicateObject","s3:PutObjectRetention","s3:PutObjectLegalHold"`) + + // Bucket default retention, so a marker-only write with no lock headers still + // has a validated retention to apply. + lockCfg := []byte(`EnabledGOVERNANCE30`) + if _, err := globalBucketMetadataSys.Update(t.Context(), bucketName, objectLockConfig, lockCfg); err != nil { + t.Fatalf("%s: configure bucket default retention: %v", instanceType, err) + } + + // markerOnly returns the trusted-but-not-REPLICA header set plus extra: the + // internal marker with no REPLICA status. + markerOnly := func(extra map[string]string) map[string]string { + h := map[string]string{xhttp.MinIOSourceReplicationRequest: "true"} + maps.Copy(h, extra) + return h + } + data := bytes.Repeat([]byte("marker-only-lock-"), 32) + + markerOnlyMPU := func(t *testing.T, object string, lockHeaders map[string]string) { + t.Helper() + newReq, err := newTestSignedRequestV4(http.MethodPost, getNewMultipartURL("", bucketName, object), 0, nil, + peer.AccessKey, peer.SecretKey, markerOnly(lockHeaders)) + if err != nil { + t.Fatal(err) + } + newRec := httptest.NewRecorder() + apiRouter.ServeHTTP(newRec, newReq) + if newRec.Code != http.StatusOK { + t.Fatalf("%s: marker-only NewMultipartUpload %d: %s", instanceType, newRec.Code, newRec.Body.String()) + } + var init InitiateMultipartUploadResponse + if err = xmlDecoder(newRec.Body, &init, int64(newRec.Body.Len())); err != nil { + t.Fatal(err) + } + partReq, err := newTestSignedRequestV4(http.MethodPut, + getPutObjectPartURL("", bucketName, object, init.UploadID, "1"), int64(len(data)), + bytes.NewReader(data), peer.AccessKey, peer.SecretKey, nil) + if err != nil { + t.Fatal(err) + } + partRec := httptest.NewRecorder() + apiRouter.ServeHTTP(partRec, partReq) + if partRec.Code != http.StatusOK { + t.Fatalf("%s: marker-only PutObjectPart %d: %s", instanceType, partRec.Code, partRec.Body.String()) + } + body, err := xml.Marshal(CompleteMultipartUpload{Parts: []CompletePart{ + {PartNumber: 1, ETag: canonicalizeETag(partRec.Header()[xhttp.ETag][0])}, + }}) + if err != nil { + t.Fatal(err) + } + completeReq, err := newTestSignedRequestV4(http.MethodPost, + getCompleteMultipartUploadURL("", bucketName, object, init.UploadID), int64(len(body)), + bytes.NewReader(body), peer.AccessKey, peer.SecretKey, nil) + if err != nil { + t.Fatal(err) + } + completeRec := httptest.NewRecorder() + apiRouter.ServeHTTP(completeRec, completeReq) + if completeRec.Code != http.StatusOK { + t.Fatalf("%s: marker-only CompleteMultipartUpload %d: %s", instanceType, completeRec.Code, completeRec.Body.String()) + } + } + markerOnlyPUT := func(t *testing.T, object string, lockHeaders map[string]string) { + t.Helper() + req, err := newTestSignedRequestV4(http.MethodPut, getPutObjectURL("", bucketName, object), int64(len(data)), + bytes.NewReader(data), peer.AccessKey, peer.SecretKey, markerOnly(lockHeaders)) + if err != nil { + t.Fatal(err) + } + rec := httptest.NewRecorder() + apiRouter.ServeHTTP(rec, req) + if rec.Code != http.StatusOK { + t.Fatalf("%s: marker-only PUT %d: %s", instanceType, rec.Code, rec.Body.String()) + } + } + lockOf := func(t *testing.T, object string) objectLockState { + t.Helper() + info, err := obj.GetObjectInfo(t.Context(), bucketName, object, ObjectOptions{}) + if err != nil { + t.Fatal(err) + } + return storedObjectLockState(info.UserDefined) + } + + // An explicit legal hold on a marker-only write must be applied, not dropped. + // A legal-hold request suppresses the bucket default retention, so the version + // carries only the hold. + explicitHold := map[string]string{xhttp.AmzObjectLockLegalHold: "ON"} + + t.Run("put-explicit-legal-hold", func(t *testing.T) { + object := "marker-only/put-legal-hold" + markerOnlyPUT(t, object, explicitHold) + if got := lockOf(t, object); got.legalHold != "ON" { + t.Fatalf("%s: marker-only PUT dropped the explicit legal hold: got %+v", instanceType, got) + } + }) + t.Run("mpu-explicit-legal-hold", func(t *testing.T) { + object := "marker-only/mpu-legal-hold" + markerOnlyMPU(t, object, explicitHold) + if got := lockOf(t, object); got.legalHold != "ON" { + t.Fatalf("%s: marker-only multipart dropped the explicit legal hold: got %+v", instanceType, got) + } + }) + t.Run("put-bucket-default-retention", func(t *testing.T) { + object := "marker-only/put-default-retention" + markerOnlyPUT(t, object, nil) + if got := lockOf(t, object); got.mode != "GOVERNANCE" || got.retainUntil == "" { + t.Fatalf("%s: marker-only PUT dropped the bucket default retention: got %+v", instanceType, got) + } + }) + t.Run("mpu-bucket-default-retention", func(t *testing.T) { + object := "marker-only/mpu-default-retention" + markerOnlyMPU(t, object, nil) + if got := lockOf(t, object); got.mode != "GOVERNANCE" || got.retainUntil == "" { + t.Fatalf("%s: marker-only multipart dropped the bucket default retention: got %+v", instanceType, got) + } + }) +} + +// TestAPIReplicaMultipartNewerHoldSurvivesCompletion verifies that a legal hold +// that reaches a destination version AFTER a replica multipart upload was +// initiated is not rolled back when that upload completes. The initiation +// resolves the lock against the version as it then stands, but completion +// re-orders the carried lock against the version read under the namespace write +// lock that guards the replacement, so a newer hold (with its newer timestamp) +// survives. See pgsty/silo#120 (Codex finding 1). +func TestAPIReplicaMultipartNewerHoldSurvivesCompletion(t *testing.T) { + defer DetectTestLeak(t)() + ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{ + t: t, + objAPITest: testAPIReplicaMultipartNewerHoldSurvivesCompletion, + makeBucketOptions: MakeBucketOptions{LockEnabled: true}, + }) +} + +func testAPIReplicaMultipartNewerHoldSurvivesCompletion(obj ObjectLayer, instanceType, bucketName string, + apiRouter http.Handler, credentials auth.Credentials, t *testing.T, +) { + previousTLS := globalIsTLS + globalIsTLS = true + defer func() { globalIsTLS = previousTLS }() + + key := bytes.Repeat([]byte{0x47}, 32) + keyMD5 := md5.Sum(key) + sseHeaders := map[string]string{ + xhttp.AmzServerSideEncryptionCustomerAlgorithm: xhttp.AmzEncryptionAES, + xhttp.AmzServerSideEncryptionCustomerKey: base64.StdEncoding.EncodeToString(key), + xhttp.AmzServerSideEncryptionCustomerKeyMD5: base64.StdEncoding.EncodeToString(keyMD5[:]), + } + object := "mpu-lock-boundary/obj" + + // seal returns the sender's SSE-C replica seal and marker headers for the + // addressed version, with every Object Lock header stripped so the case owns + // the lock instruction. + seal := func(t *testing.T, srcInfo ObjectInfo) map[string]string { + t.Helper() + opts, _, err := putReplicationOpts(t.Context(), "", srcInfo) + if err != nil { + t.Fatal(err) + } + opts.Internal.SourceMTime = time.Time{} + hdrs := make(map[string]string) + for name, values := range opts.Header() { + if len(values) > 0 { + hdrs[name] = values[0] + } + } + hdrs[xhttp.MinIOSourceReplicationRequest] = "true" + hdrs[xhttp.AmzBucketReplicationStatus] = "REPLICA" + hdrs[xhttp.MinIOSourceETag] = srcInfo.ETag + for _, name := range []string{ + xhttp.AmzObjectLockMode, xhttp.AmzObjectLockRetainUntilDate, xhttp.AmzObjectLockLegalHold, + xhttp.MinIOSourceObjectRetentionTimestamp, xhttp.MinIOSourceObjectLegalHoldTimestamp, + } { + delete(hdrs, name) + } + return hdrs + } + rawOf := func(t *testing.T, versionID string) (ObjectInfo, []byte) { + t.Helper() + gr, err := obj.GetObjectNInfo(t.Context(), bucketName, object, nil, http.Header{}, ObjectOptions{ + ReplicationRequest: true, VersionID: versionID, + }) + if err != nil { + t.Fatal(err) + } + srcInfo := gr.ObjInfo + cipher, rerr := io.ReadAll(gr) + gr.Close() + if rerr != nil { + t.Fatal(rerr) + } + return srcInfo, cipher + } + + // A destination SSE-C version with no lock. + putCopyChecksumSource(t, apiRouter, credentials, bucketName, object, + bytes.Repeat([]byte("mpu-lock-boundary-"), 64), sseHeaders) + base, err := obj.GetObjectInfo(t.Context(), bucketName, object, ObjectOptions{}) + if err != nil { + t.Fatal(err) + } + versionID := base.VersionID + srcInfo, cipher := rawOf(t, versionID) + + // Initiate an SSE-C replica multipart carrying legal hold OFF stamped 09:00. + // The destination has no lock yet, so the decision made now is OFF@09:00. + newHdrs := seal(t, srcInfo) + newHdrs[xhttp.AmzObjectLockLegalHold] = "OFF" + newHdrs[xhttp.MinIOSourceObjectLegalHoldTimestamp] = objectLockTestStamp0900 + newReq, err := newTestSignedRequestV4(http.MethodPost, + getNewMultipartURL("", bucketName, object)+"&versionId="+versionID, 0, nil, + credentials.AccessKey, credentials.SecretKey, newHdrs) + if err != nil { + t.Fatal(err) + } + newRec := httptest.NewRecorder() + apiRouter.ServeHTTP(newRec, newReq) + if newRec.Code != http.StatusOK { + t.Fatalf("%s: replica NewMultipartUpload %d: %s", instanceType, newRec.Code, newRec.Body.String()) + } + var init InitiateMultipartUploadResponse + if err = xmlDecoder(newRec.Body, &init, int64(newRec.Body.Len())); err != nil { + t.Fatal(err) + } + + // After initiation, a newer legal hold ON@10:00 lands on the same version + // through an independent SSE-C replica PUT retransmit. + putHdrs := seal(t, srcInfo) + putHdrs[xhttp.AmzObjectLockLegalHold] = "ON" + putHdrs[xhttp.MinIOSourceObjectLegalHoldTimestamp] = objectLockTestStamp1000 + putReq, err := newTestSignedRequestV4(http.MethodPut, + getPutObjectURL("", bucketName, object)+"?versionId="+versionID, int64(len(cipher)), + bytes.NewReader(cipher), credentials.AccessKey, credentials.SecretKey, putHdrs) + if err != nil { + t.Fatal(err) + } + putRec := httptest.NewRecorder() + apiRouter.ServeHTTP(putRec, putReq) + if putRec.Code != http.StatusOK { + t.Fatalf("%s: interleaving replica PUT %d: %s", instanceType, putRec.Code, putRec.Body.String()) + } + if got := readObjectLockFields(t, obj, bucketName, object, versionID); got.legalHold != "ON" { + t.Fatalf("%s: the interleaving PUT did not set the newer ON: got %+v", instanceType, got) + } + + // Finish the multipart upload the sender's way and prove the newer ON survives. + partReq, err := newTestSignedRequestV4(http.MethodPut, + getPutObjectPartURL("", bucketName, object, init.UploadID, "1"), int64(len(cipher)), + bytes.NewReader(cipher), credentials.AccessKey, credentials.SecretKey, + map[string]string{xhttp.MinIOSourceReplicationRequest: "true"}) + if err != nil { + t.Fatal(err) + } + partRec := httptest.NewRecorder() + apiRouter.ServeHTTP(partRec, partReq) + if partRec.Code != http.StatusOK { + t.Fatalf("%s: replica PutObjectPart %d: %s", instanceType, partRec.Code, partRec.Body.String()) + } + actualSize, err := srcInfo.GetActualSize() + if err != nil { + t.Fatal(err) + } + body, err := xml.Marshal(CompleteMultipartUpload{Parts: []CompletePart{ + {PartNumber: 1, ETag: canonicalizeETag(partRec.Header()[xhttp.ETag][0])}, + }}) + if err != nil { + t.Fatal(err) + } + completeReq, err := newTestSignedRequestV4(http.MethodPost, + getCompleteMultipartUploadURL("", bucketName, object, init.UploadID), int64(len(body)), + bytes.NewReader(body), credentials.AccessKey, credentials.SecretKey, map[string]string{ + xhttp.MinIOSourceReplicationRequest: "true", + xhttp.MinIOSourceMTime: srcInfo.ModTime.Format(time.RFC3339Nano), + xhttp.MinIOSourceETag: srcInfo.ETag, + xhttp.MinIOReplicationActualObjectSize: strconv.FormatInt(actualSize, 10), + }) + if err != nil { + t.Fatal(err) + } + completeRec := httptest.NewRecorder() + apiRouter.ServeHTTP(completeRec, completeReq) + if completeRec.Code != http.StatusOK { + t.Fatalf("%s: replica CompleteMultipartUpload %d: %s", instanceType, completeRec.Code, completeRec.Body.String()) + } + + // The completion re-ordered the carried OFF@09:00 against the ON@10:00 that + // reached the version after initiation: the newer hold and its timestamp win. + want := objectLockFields{legalHold: "ON", legalHoldStamp: objectLockTestStamp1000} + if got := readObjectLockFields(t, obj, bucketName, object, versionID); got != want { + t.Fatalf("%s: a newer legal hold that arrived after initiation was rolled back at completion: got %+v, want %+v", + instanceType, got, want) + } +} + +// TestReplicaPutObjectLockReconcileUnderWriteLock exercises the PUT counterpart +// of the multipart reconcile: a replica full write reaches the object layer +// carrying the Object Lock its handler resolved, but the addressed version has +// since taken a newer lock update. PutObject must re-order the incoming lock +// against the version read under the write lock, so the newer stored value is +// kept. Driving the object layer directly stands in for the handler-read / +// backend-commit interleave without a timing race. See pgsty/silo#120. +func TestReplicaPutObjectLockReconcileUnderWriteLock(t *testing.T) { + defer DetectTestLeak(t)() + ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{ + t: t, + objAPITest: testReplicaPutObjectLockReconcileUnderWriteLock, + makeBucketOptions: MakeBucketOptions{LockEnabled: true}, + }) +} + +func testReplicaPutObjectLockReconcileUnderWriteLock(obj ObjectLayer, instanceType, bucketName string, + _ http.Handler, _ auth.Credentials, t *testing.T, +) { + ctx := t.Context() + + seedVersion := func(t *testing.T, object string, meta map[string]string) string { + t.Helper() + info, err := obj.PutObject(ctx, bucketName, object, + mustGetPutObjReader(t, bytes.NewReader([]byte("data")), 4, "", ""), + ObjectOptions{Versioned: true, UserDefined: meta}) + if err != nil { + t.Fatal(err) + } + return info.VersionID + } + // replicaWrite overwrites the addressed version the way a replica retransmit + // reaches the object layer, with the in-lock reconcile enabled. + replicaWrite := func(t *testing.T, object, versionID string, meta map[string]string) { + t.Helper() + _, err := obj.PutObject(ctx, bucketName, object, + mustGetPutObjReader(t, bytes.NewReader([]byte("data")), 4, "", ""), + ObjectOptions{Versioned: true, VersionID: versionID, ReplicaLockReconcile: true, UserDefined: meta}) + if err != nil { + t.Fatal(err) + } + } + + t.Run("older-legal-hold-off-does-not-clear-newer-on", func(t *testing.T) { + object := "reconcile/put-legal-hold" + versionID := seedVersion(t, object, map[string]string{ + strings.ToLower(xhttp.AmzObjectLockLegalHold): "ON", + ReservedMetadataPrefixLower + ObjectLockLegalHoldTimestamp: objectLockTestStamp1000, + }) + replicaWrite(t, object, versionID, map[string]string{ + strings.ToLower(xhttp.AmzObjectLockLegalHold): "OFF", + ReservedMetadataPrefixLower + ObjectLockLegalHoldTimestamp: objectLockTestStamp0900, + }) + want := objectLockFields{legalHold: "ON", legalHoldStamp: objectLockTestStamp1000} + if got := readObjectLockFields(t, obj, bucketName, object, versionID); got != want { + t.Fatalf("%s: in-lock reconcile let an older OFF overwrite the newer stored ON: got %+v, want %+v", + instanceType, got, want) + } + }) + + t.Run("stale-retention-does-not-overwrite-newer", func(t *testing.T) { + object := "reconcile/put-retention" + versionID := seedVersion(t, object, map[string]string{ + strings.ToLower(xhttp.AmzObjectLockMode): "GOVERNANCE", + strings.ToLower(xhttp.AmzObjectLockRetainUntilDate): retransmitRetainUntilNewer, + ReservedMetadataPrefixLower + ObjectLockRetentionTimestamp: objectLockTestStamp1000, + }) + replicaWrite(t, object, versionID, map[string]string{ + strings.ToLower(xhttp.AmzObjectLockMode): "GOVERNANCE", + strings.ToLower(xhttp.AmzObjectLockRetainUntilDate): retransmitRetainUntilStale, + ReservedMetadataPrefixLower + ObjectLockRetentionTimestamp: objectLockTestStamp0900, + }) + want := objectLockFields{ + mode: "GOVERNANCE", retainUntil: retransmitRetainUntilNewer, retentionStamp: objectLockTestStamp1000, + } + if got := readObjectLockFields(t, obj, bucketName, object, versionID); got != want { + t.Fatalf("%s: in-lock reconcile let a stale retention overwrite the newer stored one: got %+v, want %+v", + instanceType, got, want) + } + }) + + t.Run("newer-incoming-hold-applies", func(t *testing.T) { + object := "reconcile/put-newer-applies" + versionID := seedVersion(t, object, map[string]string{ + strings.ToLower(xhttp.AmzObjectLockLegalHold): "OFF", + ReservedMetadataPrefixLower + ObjectLockLegalHoldTimestamp: objectLockTestStamp0900, + }) + replicaWrite(t, object, versionID, map[string]string{ + strings.ToLower(xhttp.AmzObjectLockLegalHold): "ON", + ReservedMetadataPrefixLower + ObjectLockLegalHoldTimestamp: objectLockTestStamp1000, + }) + want := objectLockFields{legalHold: "ON", legalHoldStamp: objectLockTestStamp1000} + if got := readObjectLockFields(t, obj, bucketName, object, versionID); got != want { + t.Fatalf("%s: in-lock reconcile did not apply the newer incoming hold: got %+v, want %+v", + instanceType, got, want) + } + }) + + t.Run("pre-upgrade-shape-on-absent-version-is-preserved", func(t *testing.T) { + // A pre-upgrade upload persisted validated lock values WITHOUT their + // ordering timestamps. Completing it while the destination version is + // absent must keep those values: there is nothing to order against, so the + // reconcile is skipped rather than deleting the accepted lock. The same + // not-found handling guards CompleteMultipartUpload. + object := "reconcile/put-absent-version" + absentVersion := mustGetUUID() + _, err := obj.PutObject(ctx, bucketName, object, + mustGetPutObjReader(t, bytes.NewReader([]byte("data")), 4, "", ""), + ObjectOptions{Versioned: true, VersionID: absentVersion, ReplicaLockReconcile: true, UserDefined: map[string]string{ + strings.ToLower(xhttp.AmzObjectLockMode): "GOVERNANCE", + strings.ToLower(xhttp.AmzObjectLockRetainUntilDate): retransmitRetainUntilNewer, + strings.ToLower(xhttp.AmzObjectLockLegalHold): "ON", + }}) + if err != nil { + t.Fatal(err) + } + want := objectLockFields{mode: "GOVERNANCE", retainUntil: retransmitRetainUntilNewer, legalHold: "ON"} + if got := readObjectLockFields(t, obj, bucketName, object, absentVersion); got != want { + t.Fatalf("%s: a pre-upgrade lock (no ordering timestamps) on an absent version was stripped: got %+v, want %+v", + instanceType, got, want) + } + }) +} + +// TestReplicaLockReconcileNullVersion covers the null version, which persisted +// upload metadata records as an empty VersionID. The completion reconcile must +// order the incoming lock against the null version's own stored state -- looked +// up as the null version, not the latest -- so a retransmit addressing the null +// version cannot be reconciled against an unrelated UUID version, and a null +// version absent while a UUID version exists is not mistaken for present. Single +// erasure set. See pgsty/silo#120. +func TestReplicaLockReconcileNullVersion(t *testing.T) { + defer DetectTestLeak(t)() + ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{ + t: t, + objAPITest: testReplicaLockReconcileNullVersion, + makeBucketOptions: MakeBucketOptions{VersioningEnabled: true}, + }) +} + +func testReplicaLockReconcileNullVersion(obj ObjectLayer, instanceType, bucketName string, + _ http.Handler, _ auth.Credentials, t *testing.T, +) { + ctx := t.Context() + + // completeNullMPU runs an SSE-C replica multipart upload addressing the null + // version (VersionSuspended) carrying uploadLock, through the reconcile. + completeNullMPU := func(t *testing.T, object string, uploadLock map[string]string) { + t.Helper() + meta := map[string]string{crypto.MetaSealedKeySSEC: "dummy-sealed-key"} + maps.Copy(meta, uploadLock) + res, err := obj.NewMultipartUpload(ctx, bucketName, object, ObjectOptions{VersionSuspended: true, UserDefined: meta}) + if err != nil { + t.Fatal(err) + } + // PutObjectPart now validates the DARE stream length (#119). This + // object-layer fixture needs an encrypted body before it can exercise + // the completion-time null-version metadata reconciliation. + var ciphertext bytes.Buffer + if _, err := sio.Encrypt(&ciphertext, bytes.NewReader([]byte("data")), sio.Config{Key: bytes.Repeat([]byte{1}, 32)}); err != nil { + t.Fatal(err) + } + part, err := obj.PutObjectPart(ctx, bucketName, object, res.UploadID, 1, + mustGetPutObjReader(t, bytes.NewReader(ciphertext.Bytes()), int64(ciphertext.Len()), "", ""), ObjectOptions{}) + if err != nil { + t.Fatal(err) + } + if _, err := obj.CompleteMultipartUpload(ctx, bucketName, object, res.UploadID, + []CompletePart{{PartNumber: 1, ETag: part.ETag}}, + ObjectOptions{VersionSuspended: true, ReplicaLockReconcile: true}); err != nil { + t.Fatal(err) + } + } + + t.Run("null-and-uuid-present-reconciles-the-null-version", func(t *testing.T) { + object := "reconcile/null-vs-uuid" + // The null version holds a newer legal hold ON@10:00. + if _, err := obj.PutObject(ctx, bucketName, object, + mustGetPutObjReader(t, bytes.NewReader([]byte("data")), 4, "", ""), + ObjectOptions{VersionSuspended: true, MTime: time.Date(2026, 9, 3, 10, 0, 0, 0, time.UTC), UserDefined: map[string]string{ + strings.ToLower(xhttp.AmzObjectLockLegalHold): "ON", + ReservedMetadataPrefixLower + ObjectLockLegalHoldTimestamp: objectLockTestStamp1000, + }}); err != nil { + t.Fatal(err) + } + // A later UUID version holds an unrelated OFF@11:00 and is the latest. + uuidInfo, err := obj.PutObject(ctx, bucketName, object, + mustGetPutObjReader(t, bytes.NewReader([]byte("data")), 4, "", ""), + ObjectOptions{Versioned: true, MTime: time.Date(2026, 9, 3, 11, 0, 0, 0, time.UTC), UserDefined: map[string]string{ + strings.ToLower(xhttp.AmzObjectLockLegalHold): "OFF", + ReservedMetadataPrefixLower + ObjectLockLegalHoldTimestamp: objectLockTestStamp1100, + }}) + if err != nil { + t.Fatal(err) + } + + // A null-version SSE-C retransmit carrying an older OFF@09:00. + completeNullMPU(t, object, map[string]string{ + strings.ToLower(xhttp.AmzObjectLockLegalHold): "OFF", + ReservedMetadataPrefixLower + ObjectLockLegalHoldTimestamp: objectLockTestStamp0900, + }) + + // The null version keeps its own newer ON@10:00; the UUID is untouched. + wantNull := objectLockFields{legalHold: "ON", legalHoldStamp: objectLockTestStamp1000} + if got := readObjectLockFields(t, obj, bucketName, object, nullVersionID); got != wantNull { + t.Fatalf("%s: null-version completion reconciled against the wrong version: got %+v, want %+v", instanceType, got, wantNull) + } + wantUUID := objectLockFields{legalHold: "OFF", legalHoldStamp: objectLockTestStamp1100} + if got := readObjectLockFields(t, obj, bucketName, object, uuidInfo.VersionID); got != wantUUID { + t.Fatalf("%s: the UUID version was changed by a null-version completion: got %+v, want %+v", instanceType, got, wantUUID) + } + }) + + t.Run("absent-null-with-uuid-keeps-accepted-lock", func(t *testing.T) { + object := "reconcile/null-absent" + // Only a UUID version exists; there is no null version. + if _, err := obj.PutObject(ctx, bucketName, object, + mustGetPutObjReader(t, bytes.NewReader([]byte("data")), 4, "", ""), + ObjectOptions{Versioned: true, MTime: time.Date(2026, 9, 3, 11, 0, 0, 0, time.UTC), UserDefined: map[string]string{ + strings.ToLower(xhttp.AmzObjectLockLegalHold): "OFF", + ReservedMetadataPrefixLower + ObjectLockLegalHoldTimestamp: objectLockTestStamp1100, + }}); err != nil { + t.Fatal(err) + } + + // A null-version retransmit with its own validated legal hold ON. The null + // version does not exist, so the write must keep its accepted lock rather + // than order against the unrelated UUID version. + completeNullMPU(t, object, map[string]string{ + strings.ToLower(xhttp.AmzObjectLockLegalHold): "ON", + }) + if got := readObjectLockFields(t, obj, bucketName, object, nullVersionID); got.legalHold != "ON" { + t.Fatalf("%s: an absent null version was reconciled against the UUID version: got %+v", instanceType, got) + } + }) +} diff --git a/cmd/replication-trust-ssec-replica_test.go b/cmd/replication-trust-ssec-replica_test.go new file mode 100644 index 000000000..9e42e93bd --- /dev/null +++ b/cmd/replication-trust-ssec-replica_test.go @@ -0,0 +1,558 @@ +// Copyright (c) 2015-2026 MinIO, Inc. +// Copyright (c) 2026 PGSTY +// +// This file is part of MinIO Object Storage stack +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +package cmd + +import ( + "bytes" + "crypto/md5" + "encoding/base64" + "encoding/xml" + "io" + "net/http" + "net/http/httptest" + "strconv" + "testing" + "time" + + "github.com/minio/minio/internal/auth" + "github.com/minio/minio/internal/crypto" + xhttp "github.com/minio/minio/internal/http" + "github.com/minio/minio/internal/kms" +) + +// TestAPISSECReplicaSkipsDestinationTransforms asserts that a validated raw +// SSE-C replica write is stored byte for byte, whatever default encryption or +// compression the destination bucket has configured, and that the exemption is +// gated on replication trust rather than on the headers alone. +func TestAPISSECReplicaSkipsDestinationTransforms(t *testing.T) { + defer DetectTestLeak(t)() + ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{ + t: t, + objAPITest: testAPISSECReplicaSkipsDestinationTransforms, + }) +} + +func testAPISSECReplicaSkipsDestinationTransforms(obj ObjectLayer, instanceType, bucketName string, + apiRouter http.Handler, credentials auth.Credentials, t *testing.T, +) { + previousTLS := globalIsTLS + globalIsTLS = true + defer func() { globalIsTLS = previousTLS }() + + replicator := newObjectAttributesAuthzUser(t, instanceType, bucketName, `"s3:PutObject","s3:GetObject","s3:ReplicateObject"`) + putOnly := newObjectAttributesAuthzUser(t, instanceType, bucketName, `"s3:PutObject","s3:GetObject"`) + + key := bytes.Repeat([]byte{0x42}, 32) + keyMD5 := md5.Sum(key) + sseHeaders := map[string]string{ + xhttp.AmzServerSideEncryptionCustomerAlgorithm: xhttp.AmzEncryptionAES, + xhttp.AmzServerSideEncryptionCustomerKey: base64.StdEncoding.EncodeToString(key), + xhttp.AmzServerSideEncryptionCustomerKeyMD5: base64.StdEncoding.EncodeToString(keyMD5[:]), + } + data := bytes.Repeat([]byte("silo ssec replica payload "), 8192) // > minCompressibleSize + + // makeSource writes a real client SSE-C object, then reads it back the way + // the replication worker does and builds the replica wire headers from the + // production option builder. The SSE-C sealed key is bound to bucket and + // object path, so a replica is only unsealable at the same key: the caller + // writes the raw bytes back over the same name, as a real destination does. + makeSource := func(t *testing.T, object string) ([]byte, map[string]string, ObjectInfo) { + t.Helper() + srcReq, err := newTestSignedRequestV4(http.MethodPut, getPutObjectURL("", bucketName, object), + int64(len(data)), bytes.NewReader(data), credentials.AccessKey, credentials.SecretKey, sseHeaders) + if err != nil { + t.Fatal(err) + } + srcRec := httptest.NewRecorder() + apiRouter.ServeHTTP(srcRec, srcReq) + if srcRec.Code != http.StatusOK { + t.Fatalf("%s: source PUT status %d: %s", instanceType, srcRec.Code, srcRec.Body.String()) + } + + gr, err := obj.GetObjectNInfo(t.Context(), bucketName, object, nil, http.Header{}, ObjectOptions{ReplicationRequest: true}) + if err != nil { + t.Fatal(err) + } + sourceInfo := gr.ObjInfo + raw, err := io.ReadAll(gr) + gr.Close() + if err != nil { + t.Fatal(err) + } + if len(raw) == 0 || bytes.Equal(raw, data) { + t.Fatal("source replication read did not return encrypted bytes") + } + if sourceInfo.UserDefined[ReservedMetadataPrefix+"compression"] != "" { + t.Fatal("source SSE-C object was compressed; the fixture is not a raw SSE-C source") + } + replicationOpts, isMP, err := putReplicationOpts(t.Context(), "", sourceInfo) + if err != nil { + t.Fatal(err) + } + if isMP { + t.Fatal("single PUT SSE-C source was classified as multipart") + } + headers := make(map[string]string) + for name, values := range replicationOpts.Header() { + if len(values) > 0 { + headers[name] = values[0] + } + } + if headers["X-Minio-Replication-Server-Side-Encryption-Sealed-Key"] == "" { + t.Fatal("replication options did not carry the source SSE-C seal") + } + return raw, headers, sourceInfo + } + + putReplica := func(t *testing.T, object string, raw []byte, creds auth.Credentials, headers map[string]string) *httptest.ResponseRecorder { + t.Helper() + req, err := newTestSignedRequestV4(http.MethodPut, getPutObjectURL("", bucketName, object), + int64(len(raw)), bytes.NewReader(raw), creds.AccessKey, creds.SecretKey, headers) + if err != nil { + t.Fatal(err) + } + rec := httptest.NewRecorder() + apiRouter.ServeHTTP(rec, req) + return rec + } + + withDefaultSSE := func(t *testing.T) func() { + t.Helper() + previousKMS := GlobalKMS + GlobalKMS = kms.NewStub("ssec-replica-default-encryption") + sseXML := []byte(`AES256`) + if _, err := globalBucketMetadataSys.Update(t.Context(), bucketName, bucketSSEConfig, sseXML); err != nil { + t.Fatalf("configure bucket encryption: %v", err) + } + return func() { + if _, err := globalBucketMetadataSys.Delete(t.Context(), bucketName, bucketSSEConfig); err != nil { + t.Fatalf("remove bucket encryption: %v", err) + } + GlobalKMS = previousKMS + } + } + + withCompression := func(t *testing.T) func() { + t.Helper() + globalCompressConfigMu.Lock() + previous := globalCompressConfig + globalCompressConfig.Enabled = true + globalCompressConfig.Extensions = []string{".txt"} + globalCompressConfig.MimeTypes = nil + globalCompressConfig.AllowEncrypted = false + globalCompressConfigMu.Unlock() + return func() { + globalCompressConfigMu.Lock() + globalCompressConfig = previous + globalCompressConfigMu.Unlock() + } + } + + for _, tc := range []struct { + name string + setup func(*testing.T) func() + extraHeaders map[string]string + }{ + {name: "default-sse-s3", setup: withDefaultSSE}, + {name: "compression", setup: withCompression}, + // A trusted peer that also sends an explicit public SSE header must + // not re-encrypt: skipping the bucket default alone would not stop it. + {name: "explicit-sse-header", extraHeaders: map[string]string{xhttp.AmzServerSideEncryption: xhttp.AmzEncryptionAES}}, + } { + t.Run(instanceType+"/"+tc.name, func(t *testing.T) { + object := "replication/ssec-" + tc.name + ".txt" + raw, replicationHeaders, sourceInfo := makeSource(t, object) + + if tc.setup != nil { + cleanup := tc.setup(t) + defer cleanup() + } + for k, v := range tc.extraHeaders { + replicationHeaders[k] = v + } + + rec := putReplica(t, object, raw, replicator, replicationHeaders) + if rec.Code != http.StatusOK { + t.Fatalf("replica PUT status %d: %s", rec.Code, rec.Body.String()) + } + + info, err := obj.GetObjectInfo(t.Context(), bucketName, object, ObjectOptions{}) + if err != nil { + t.Fatal(err) + } + for _, name := range []string{crypto.MetaSealedKeySSEC, crypto.MetaIV, crypto.MetaAlgorithm} { + if got, want := info.UserDefined[name], sourceInfo.UserDefined[name]; got != want { + t.Errorf("%s: replica has %q, source has %q", name, got, want) + } + } + for _, name := range []string{crypto.MetaSealedKeyS3, crypto.MetaKeyID, ReservedMetadataPrefix + "compression"} { + if v, ok := info.UserDefined[name]; ok { + t.Errorf("replica gained destination metadata %s=%q", name, v) + } + } + if info.Size != int64(len(raw)) { + t.Errorf("replica size %d, want the source ciphertext size %d", info.Size, len(raw)) + } + + getReq, err := newTestSignedRequestV4(http.MethodGet, getGetObjectURL("", bucketName, object), + 0, nil, credentials.AccessKey, credentials.SecretKey, sseHeaders) + if err != nil { + t.Fatal(err) + } + getRec := httptest.NewRecorder() + apiRouter.ServeHTTP(getRec, getReq) + if getRec.Code != http.StatusOK { + t.Fatalf("GET replica status %d: %s", getRec.Code, getRec.Body.String()) + } + if !bytes.Equal(getRec.Body.Bytes(), data) { + t.Fatalf("replica did not decrypt to the source plaintext (got %d bytes, want %d)", + getRec.Body.Len(), len(data)) + } + }) + } + + // The multipart replica path shares the same two decisions at + // NewMultipartUpload. Every later part follows the upload's stored + // metadata, so asserting on the init is enough. + for _, tc := range []struct { + name string + setup func(*testing.T) func() + }{ + {name: "mpu-default-sse-s3", setup: withDefaultSSE}, + {name: "mpu-compression", setup: withCompression}, + } { + t.Run(instanceType+"/"+tc.name, func(t *testing.T) { + object := "replication/ssec-" + tc.name + ".txt" + _, replicationHeaders, sourceInfo := makeSource(t, object) + + cleanup := tc.setup(t) + defer cleanup() + + req, err := newTestSignedRequestV4(http.MethodPost, getNewMultipartURL("", bucketName, object), + 0, nil, replicator.AccessKey, replicator.SecretKey, replicationHeaders) + if err != nil { + t.Fatal(err) + } + rec := httptest.NewRecorder() + apiRouter.ServeHTTP(rec, req) + if rec.Code != http.StatusOK { + t.Fatalf("replica NewMultipart status %d: %s", rec.Code, rec.Body.String()) + } + var init InitiateMultipartUploadResponse + if err = xmlDecoder(rec.Body, &init, int64(rec.Body.Len())); err != nil { + t.Fatal(err) + } + mi, err := obj.GetMultipartInfo(t.Context(), bucketName, object, init.UploadID, ObjectOptions{}) + if err != nil { + t.Fatal(err) + } + for _, name := range []string{crypto.MetaSealedKeySSEC, crypto.MetaIV, crypto.MetaAlgorithm} { + if got, want := mi.UserDefined[name], sourceInfo.UserDefined[name]; got != want { + t.Errorf("%s: replica upload has %q, source has %q", name, got, want) + } + } + for _, name := range []string{crypto.MetaSealedKeyS3, crypto.MetaKeyID, ReservedMetadataPrefix + "compression"} { + if v, ok := mi.UserDefined[name]; ok { + t.Errorf("replica upload gained destination metadata %s=%q", name, v) + } + } + }) + } + + // Control: the same seal headers from a principal without + // s3:ReplicateObject must not buy the exemption. The headers are stripped + // and the object is transformed exactly as an ordinary upload would be. + t.Run(instanceType+"/untrusted-is-still-transformed", func(t *testing.T) { + object := "replication/ssec-untrusted.txt" + raw, replicationHeaders, _ := makeSource(t, object) + + cleanup := withCompression(t) + defer cleanup() + + untrusted := make(map[string]string, len(replicationHeaders)) + for k, v := range replicationHeaders { + untrusted[k] = v + } + delete(untrusted, xhttp.AmzBucketReplicationStatus) + + rec := putReplica(t, object, raw, putOnly, untrusted) + if rec.Code != http.StatusOK { + t.Fatalf("untrusted PUT status %d: %s", rec.Code, rec.Body.String()) + } + info, err := obj.GetObjectInfo(t.Context(), bucketName, object, ObjectOptions{}) + if err != nil { + t.Fatal(err) + } + if _, ok := info.UserDefined[crypto.MetaSealedKeySSEC]; ok { + t.Error("untrusted writer smuggled an SSE-C seal into stored metadata") + } + if _, ok := info.UserDefined[ReservedMetadataPrefix+"compression"]; !ok { + t.Error("untrusted upload skipped destination compression") + } + }) +} + +// TestAPISSECMultipartReplicaRoundTripWithCompression drives a full multipart +// replica: a real SSE-C multipart source, the replica upload, its parts and +// Complete, then a GET with the customer key, all with destination compression +// enabled. Asserting on the init metadata alone would not prove that the parts +// were stored unmodified. +func TestAPISSECMultipartReplicaRoundTripWithCompression(t *testing.T) { + defer DetectTestLeak(t)() + ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{ + t: t, + objAPITest: testAPISSECMultipartReplicaRoundTripWithCompression, + }) +} + +func testAPISSECMultipartReplicaRoundTripWithCompression(obj ObjectLayer, instanceType, bucketName string, + apiRouter http.Handler, credentials auth.Credentials, t *testing.T, +) { + previousTLS := globalIsTLS + globalIsTLS = true + defer func() { globalIsTLS = previousTLS }() + + replicator := newObjectAttributesAuthzUser(t, instanceType, bucketName, `"s3:PutObject","s3:GetObject","s3:ReplicateObject"`) + key := bytes.Repeat([]byte{0x37}, 32) + keyMD5 := md5.Sum(key) + sseHeaders := map[string]string{ + xhttp.AmzServerSideEncryptionCustomerAlgorithm: xhttp.AmzEncryptionAES, + xhttp.AmzServerSideEncryptionCustomerKey: base64.StdEncoding.EncodeToString(key), + xhttp.AmzServerSideEncryptionCustomerKeyMD5: base64.StdEncoding.EncodeToString(keyMD5[:]), + } + data := bytes.Repeat([]byte("silo ssec multipart replica payload "), 4096) + object := "replication/ssec-multipart-roundtrip.txt" + + // Source: a real SSE-C multipart object, so the part layout and metadata + // are what the replication worker actually reads. + newRec := httptest.NewRecorder() + newReq, err := newTestSignedRequestV4(http.MethodPost, getNewMultipartURL("", bucketName, object), + 0, nil, credentials.AccessKey, credentials.SecretKey, sseHeaders) + if err != nil { + t.Fatal(err) + } + apiRouter.ServeHTTP(newRec, newReq) + if newRec.Code != http.StatusOK { + t.Fatalf("source NewMultipart status %d: %s", newRec.Code, newRec.Body.String()) + } + var sourceInit InitiateMultipartUploadResponse + if err = xmlDecoder(newRec.Body, &sourceInit, int64(newRec.Body.Len())); err != nil { + t.Fatal(err) + } + partReq, err := newTestSignedRequestV4(http.MethodPut, + getPutObjectPartURL("", bucketName, object, sourceInit.UploadID, "1"), + int64(len(data)), bytes.NewReader(data), credentials.AccessKey, credentials.SecretKey, sseHeaders) + if err != nil { + t.Fatal(err) + } + partRec := httptest.NewRecorder() + apiRouter.ServeHTTP(partRec, partReq) + if partRec.Code != http.StatusOK { + t.Fatalf("source PutPart status %d: %s", partRec.Code, partRec.Body.String()) + } + completeBody, err := xml.Marshal(CompleteMultipartUpload{Parts: []CompletePart{ + {PartNumber: 1, ETag: canonicalizeETag(partRec.Header()[xhttp.ETag][0])}, + }}) + if err != nil { + t.Fatal(err) + } + completeReq, err := newTestSignedRequestV4(http.MethodPost, + getCompleteMultipartUploadURL("", bucketName, object, sourceInit.UploadID), + int64(len(completeBody)), bytes.NewReader(completeBody), credentials.AccessKey, credentials.SecretKey, sseHeaders) + if err != nil { + t.Fatal(err) + } + completeRec := httptest.NewRecorder() + apiRouter.ServeHTTP(completeRec, completeReq) + if completeRec.Code != http.StatusOK { + t.Fatalf("source Complete status %d: %s", completeRec.Code, completeRec.Body.String()) + } + + gr, err := obj.GetObjectNInfo(t.Context(), bucketName, object, nil, http.Header{}, ObjectOptions{ReplicationRequest: true}) + if err != nil { + t.Fatal(err) + } + sourceInfo := gr.ObjInfo + rawPart, err := io.ReadAll(gr) + gr.Close() + if err != nil { + t.Fatal(err) + } + replicationOpts, isMP, err := putReplicationOpts(t.Context(), "", sourceInfo) + if err != nil { + t.Fatal(err) + } + if !isMP { + t.Fatal("SSE-C multipart source was not recognized as multipart") + } + replicationOpts.Internal.SourceMTime = time.Time{} + replicationHeaders := make(map[string]string) + for name, values := range replicationOpts.Header() { + if len(values) > 0 { + replicationHeaders[name] = values[0] + } + } + + // Destination compression on for the whole replica write. + globalCompressConfigMu.Lock() + previousCompress := globalCompressConfig + globalCompressConfig.Enabled = true + globalCompressConfig.Extensions = []string{".txt"} + globalCompressConfig.MimeTypes = nil + globalCompressConfig.AllowEncrypted = false + globalCompressConfigMu.Unlock() + defer func() { + globalCompressConfigMu.Lock() + globalCompressConfig = previousCompress + globalCompressConfigMu.Unlock() + }() + + replNewReq, err := newTestSignedRequestV4(http.MethodPost, getNewMultipartURL("", bucketName, object), + 0, nil, replicator.AccessKey, replicator.SecretKey, replicationHeaders) + if err != nil { + t.Fatal(err) + } + replNewRec := httptest.NewRecorder() + apiRouter.ServeHTTP(replNewRec, replNewReq) + if replNewRec.Code != http.StatusOK { + t.Fatalf("replica NewMultipart status %d: %s", replNewRec.Code, replNewRec.Body.String()) + } + var replicaInit InitiateMultipartUploadResponse + if err = xmlDecoder(replNewRec.Body, &replicaInit, int64(replNewRec.Body.Len())); err != nil { + t.Fatal(err) + } + mi, err := obj.GetMultipartInfo(t.Context(), bucketName, object, replicaInit.UploadID, ObjectOptions{}) + if err != nil { + t.Fatal(err) + } + if _, ok := mi.UserDefined[ReservedMetadataPrefix+"compression"]; ok { + t.Fatal("replica upload was marked compressed") + } + if _, ok := mi.UserDefined[ReservedMetadataPrefix+"Encrypted-Multipart"]; !ok { + t.Error("replica upload lost the encrypted-multipart marker") + } + + replPartReq, err := newTestSignedRequestV4(http.MethodPut, + getPutObjectPartURL("", bucketName, object, replicaInit.UploadID, "1"), + int64(len(rawPart)), bytes.NewReader(rawPart), replicator.AccessKey, replicator.SecretKey, + map[string]string{xhttp.MinIOSourceReplicationRequest: "true"}) + if err != nil { + t.Fatal(err) + } + replPartRec := httptest.NewRecorder() + apiRouter.ServeHTTP(replPartRec, replPartReq) + if replPartRec.Code != http.StatusOK { + t.Fatalf("replica PutPart status %d: %s", replPartRec.Code, replPartRec.Body.String()) + } + replCompleteBody, err := xml.Marshal(CompleteMultipartUpload{Parts: []CompletePart{ + {PartNumber: 1, ETag: canonicalizeETag(replPartRec.Header()[xhttp.ETag][0])}, + }}) + if err != nil { + t.Fatal(err) + } + actualSize, err := sourceInfo.GetActualSize() + if err != nil { + t.Fatal(err) + } + replCompleteReq, err := newTestSignedRequestV4(http.MethodPost, + getCompleteMultipartUploadURL("", bucketName, object, replicaInit.UploadID), + int64(len(replCompleteBody)), bytes.NewReader(replCompleteBody), replicator.AccessKey, replicator.SecretKey, + map[string]string{ + xhttp.MinIOSourceReplicationRequest: "true", + xhttp.MinIOSourceMTime: sourceInfo.ModTime.Format(time.RFC3339Nano), + xhttp.MinIOSourceETag: sourceInfo.ETag, + xhttp.MinIOReplicationActualObjectSize: strconv.FormatInt(actualSize, 10), + }) + if err != nil { + t.Fatal(err) + } + replCompleteRec := httptest.NewRecorder() + apiRouter.ServeHTTP(replCompleteRec, replCompleteReq) + if replCompleteRec.Code != http.StatusOK { + t.Fatalf("replica Complete status %d: %s", replCompleteRec.Code, replCompleteRec.Body.String()) + } + + info, err := obj.GetObjectInfo(t.Context(), bucketName, object, ObjectOptions{}) + if err != nil { + t.Fatal(err) + } + if v, ok := info.UserDefined[ReservedMetadataPrefix+"compression"]; ok { + t.Errorf("replica gained destination compression %q", v) + } + + getReq, err := newTestSignedRequestV4(http.MethodGet, getGetObjectURL("", bucketName, object), + 0, nil, credentials.AccessKey, credentials.SecretKey, sseHeaders) + if err != nil { + t.Fatal(err) + } + getRec := httptest.NewRecorder() + apiRouter.ServeHTTP(getRec, getReq) + if getRec.Code != http.StatusOK { + t.Fatalf("GET replicated multipart object status %d: %s", getRec.Code, getRec.Body.String()) + } + if !bytes.Equal(getRec.Body.Bytes(), data) { + t.Fatalf("replicated SSE-C multipart object did not decrypt to source plaintext (got %d bytes, want %d)", + getRec.Body.Len(), len(data)) + } +} + +// TestPutReplicationOptsRejectsCompressedSSEC covers the source-side guard: a +// compressed SSE-C object cannot be represented on the replication wire, so the +// shared option builder must refuse it rather than emit a replica that decrypts +// to an S2 stream. Uncompressed SSE-C and compressed plaintext stay accepted. +func TestPutReplicationOptsRejectsCompressedSSEC(t *testing.T) { + sealed := base64.StdEncoding.EncodeToString(bytes.Repeat([]byte{0x11}, 64)) + iv := base64.StdEncoding.EncodeToString(bytes.Repeat([]byte{0x22}, 32)) + + newInfo := func(ssec, compressed bool) ObjectInfo { + oi := ObjectInfo{ + Bucket: "src", + Name: "obj.txt", + VersionID: "v1", + Size: 1024, + ContentType: "text/plain", + UserDefined: map[string]string{}, + } + if ssec { + oi.UserDefined[crypto.MetaSealedKeySSEC] = sealed + oi.UserDefined[crypto.MetaIV] = iv + oi.UserDefined[crypto.MetaAlgorithm] = "DAREv2-HMAC-SHA256" + } + if compressed { + oi.UserDefined[ReservedMetadataPrefix+"compression"] = compressionAlgorithmV2 + oi.UserDefined[ReservedMetadataPrefix+"actual-size"] = "2048" + } + return oi + } + + for _, tc := range []struct { + name string + ssec bool + compressed bool + wantErr bool + }{ + {name: "compressed-ssec-rejected", ssec: true, compressed: true, wantErr: true}, + {name: "plain-ssec-accepted", ssec: true}, + {name: "compressed-plaintext-accepted", compressed: true}, + {name: "plain-accepted"}, + } { + t.Run(tc.name, func(t *testing.T) { + oi := newInfo(tc.ssec, tc.compressed) + if _, _, err := putReplicationOpts(t.Context(), "", oi); (err != nil) != tc.wantErr { + t.Fatalf("putReplicationOpts err=%v, wantErr=%v", err, tc.wantErr) + } + // The batch path shares the same builder and propagates its error. + if _, _, err := batchReplicationOpts(t.Context(), "", oi); (err != nil) != tc.wantErr { + t.Fatalf("batchReplicationOpts err=%v, wantErr=%v", err, tc.wantErr) + } + }) + } +} diff --git a/cmd/replication-trust.go b/cmd/replication-trust.go new file mode 100644 index 000000000..0f8cd5320 --- /dev/null +++ b/cmd/replication-trust.go @@ -0,0 +1,172 @@ +// Copyright (c) 2015-2026 MinIO, Inc. +// Copyright (c) 2026 PGSTY +// +// This file is part of MinIO Object Storage stack +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +package cmd + +import ( + "context" + "net/http" + + objectreplication "github.com/minio/minio/internal/bucket/replication" + xhttp "github.com/minio/minio/internal/http" + "github.com/minio/minio/internal/logger" + "github.com/pgsty/silo-pkg/v3/policy" +) + +type ( + replicationTrustKey struct{} + replicaTrustKey struct{} +) + +// hasReplicationMarker reports whether the internal replication marker has +// its one accepted wire value. Header presence alone is never a trust signal. +func hasReplicationMarker(h http.Header) bool { + values, ok := h[http.CanonicalHeaderKey(xhttp.MinIOSourceReplicationRequest)] + return ok && len(values) == 1 && values[0] == "true" +} + +func hasReplicationMarkerHeader(h http.Header) bool { + _, ok := h[http.CanonicalHeaderKey(xhttp.MinIOSourceReplicationRequest)] + return ok +} + +func hasReplicaStatus(h http.Header) bool { + return h.Get(xhttp.AmzBucketReplicationStatus) == objectreplication.Replica.String() +} + +func withReplicationTrust(ctx context.Context, trusted, replicaTrusted bool) context.Context { + ctx = context.WithValue(ctx, replicationTrustKey{}, trusted) + return context.WithValue(ctx, replicaTrustKey{}, trusted && replicaTrusted) +} + +func isTrustedReplication(ctx context.Context) bool { + trusted, _ := ctx.Value(replicationTrustKey{}).(bool) + return trusted +} + +func isReplicaTrusted(ctx context.Context) bool { + trusted, _ := ctx.Value(replicaTrustKey{}).(bool) + return trusted +} + +// replicationPermissionAllowed must be called only after the request's +// existing authentication/signature path has succeeded and populated ReqInfo. +// Replication peers are authenticated principals; an anonymous bucket-policy +// grant must not turn client-controlled internal headers into trusted state. +func replicationPermissionAllowed(ctx context.Context, r *http.Request, bucket, object string, action policy.Action) bool { + reqInfo := logger.GetReqInfo(ctx) + if reqInfo == nil || reqInfo.Cred.AccessKey == "" { + return false + } + reqInfo.BucketName = bucket + reqInfo.ObjectName = object + 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 +// validity/readiness probes are intentionally not listed here. +var replicationRequestHeaders = []string{ + xhttp.MinIOSourceReplicationRequest, + xhttp.MinIOSourceETag, + xhttp.MinIOSourceMTime, + xhttp.MinIOSourceDeleteMarker, + xhttp.MinIOSourceDeleteMarkerDelete, + xhttp.MinIOSourceTaggingTimestamp, + xhttp.MinIOSourceObjectRetentionTimestamp, + xhttp.MinIOSourceObjectLegalHoldTimestamp, + "X-Minio-Replication-Server-Side-Encryption-Sealed-Key", + "X-Minio-Replication-Server-Side-Encryption-Seal-Algorithm", + "X-Minio-Replication-Server-Side-Encryption-Iv", + "X-Minio-Replication-Encrypted-Multipart", + xhttp.MinIOReplicationActualObjectSize, + ReplicationSsecChecksumHeader, + xhttp.AmzBucketReplicationStatus, +} + +// ssecReplicaSealHeaders are the internal headers a source site attaches to a +// raw SSE-C replica write. Their presence means the body is source ciphertext +// that the destination can neither decrypt nor re-frame. +var ssecReplicaSealHeaders = []string{ + "X-Minio-Replication-Server-Side-Encryption-Seal-Algorithm", + "X-Minio-Replication-Server-Side-Encryption-Sealed-Key", + "X-Minio-Replication-Server-Side-Encryption-Iv", +} + +// isRawSSECReplica reports whether a request is a replica write carrying a +// source SSE-C seal. replicaTrusted must be the value evaluateReplicationTrust +// returned for this request: the headers alone are client controlled and are +// never a trust signal on their own. +func isRawSSECReplica(h http.Header, replicaTrusted bool) bool { + if !replicaTrusted { + return false + } + for _, name := range ssecReplicaSealHeaders { + if h.Get(name) != "" { + return true + } + } + return false +} + +func stripReplicationRequestHeaders(h http.Header) { + for _, name := range replicationRequestHeaders { + h.Del(name) + } +} + +func hasReplicationRequestHeaders(h http.Header) bool { + for _, name := range replicationRequestHeaders { + if _, ok := h[http.CanonicalHeaderKey(name)]; ok { + return true + } + } + return false +} + +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 +} + +// applyReplicationTrust binds the handler context to the effective request. +// The context marker is the authorization source of truth; header removal is +// defense in depth for option builders and future call sites. +func applyReplicationTrust(ctx context.Context, r *http.Request, trusted, replicaTrusted bool) (context.Context, *http.Request) { + ctx = withReplicationTrust(ctx, trusted, replicaTrusted) + if trusted { + return ctx, r.WithContext(ctx) + } + return ctx, cloneRequestWithoutReplicationHeaders(ctx, r) +} diff --git a/cmd/replication-trust_test.go b/cmd/replication-trust_test.go new file mode 100644 index 000000000..1ef38e18f --- /dev/null +++ b/cmd/replication-trust_test.go @@ -0,0 +1,1433 @@ +// Copyright (c) 2015-2026 MinIO, Inc. +// Copyright (c) 2026 PGSTY +// +// This file is part of MinIO Object Storage stack +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +package cmd + +import ( + "archive/tar" + "bytes" + "context" + "crypto/md5" + "encoding/base64" + "encoding/xml" + "io" + "net/http" + "net/http/httptest" + "net/url" + "strconv" + "strings" + "testing" + "time" + + "github.com/minio/madmin-go/v3" + "github.com/minio/minio/internal/auth" + "github.com/minio/minio/internal/crypto" + xhttp "github.com/minio/minio/internal/http" + "github.com/minio/minio/internal/kms" + "github.com/pgsty/silo-pkg/v3/policy" +) + +func TestAPIReplicationTrustProtectsSSECReads(t *testing.T) { + defer DetectTestLeak(t)() + ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{ + t: t, + objAPITest: testAPIReplicationTrustProtectsSSECReads, + }) +} + +func testAPIReplicationTrustProtectsSSECReads(_ ObjectLayer, instanceType, bucketName string, + apiRouter http.Handler, credentials auth.Credentials, t *testing.T, +) { + previousTLS := globalIsTLS + globalIsTLS = true + defer func() { globalIsTLS = previousTLS }() + + key := bytes.Repeat([]byte{0x31}, 32) + keyMD5 := md5.Sum(key) + data := bytes.Repeat([]byte("replication-trust-ssec-"), 256) + sseHeaders := map[string]string{ + xhttp.AmzServerSideEncryptionCustomerAlgorithm: xhttp.AmzEncryptionAES, + xhttp.AmzServerSideEncryptionCustomerKey: base64.StdEncoding.EncodeToString(key), + xhttp.AmzServerSideEncryptionCustomerKeyMD5: base64.StdEncoding.EncodeToString(keyMD5[:]), + } + object := "replication-trust/ssec-read" + putCopyChecksumSource(t, apiRouter, credentials, bucketName, object, data, sseHeaders) + + readerOnly := newObjectAttributesAuthzUser(t, instanceType, bucketName, `"s3:GetObject"`) + replicator := newObjectAttributesAuthzUser(t, instanceType, bucketName, `"s3:GetObject","s3:ReplicateObject"`) + + marker := map[string]string{xhttp.MinIOSourceReplicationRequest: "true"} + wrongCaseMarker := map[string]string{xhttp.MinIOSourceReplicationRequest: "TRUE"} + conditionalMarker := map[string]string{ + xhttp.MinIOSourceReplicationRequest: "true", + xhttp.IfNoneMatch: "*", + } + + for _, test := range []struct { + name string + method string + creds auth.Credentials + headers map[string]string + wantStatus int + wantPlain bool + wantCipher bool + }{ + {name: "get/reader/fake-marker", method: http.MethodGet, creds: readerOnly, headers: marker, wantStatus: http.StatusBadRequest}, + {name: "get/replicator/trusted", method: http.MethodGet, creds: replicator, headers: marker, wantStatus: http.StatusOK, wantCipher: true}, + {name: "get/root/trusted", method: http.MethodGet, creds: credentials, headers: marker, wantStatus: http.StatusOK, wantCipher: true}, + {name: "get/replicator/wrong-case", method: http.MethodGet, creds: replicator, headers: wrongCaseMarker, wantStatus: http.StatusBadRequest}, + {name: "get/reader/key", method: http.MethodGet, creds: readerOnly, headers: sseHeaders, wantStatus: http.StatusOK, wantPlain: true}, + {name: "head/reader/fake-marker", method: http.MethodHead, creds: readerOnly, headers: marker, wantStatus: http.StatusBadRequest}, + {name: "head/reader/conditional-oracle", method: http.MethodHead, creds: readerOnly, headers: conditionalMarker, wantStatus: http.StatusBadRequest}, + {name: "head/replicator/trusted", method: http.MethodHead, creds: replicator, headers: marker, wantStatus: http.StatusOK}, + {name: "head/replicator/wrong-case", method: http.MethodHead, creds: replicator, headers: wrongCaseMarker, wantStatus: http.StatusBadRequest}, + } { + t.Run(test.name, func(t *testing.T) { + req, err := newTestSignedRequestV4(test.method, getGetObjectURL("", bucketName, object), 0, nil, + test.creds.AccessKey, test.creds.SecretKey, test.headers) + if err != nil { + t.Fatal(err) + } + rec := httptest.NewRecorder() + apiRouter.ServeHTTP(rec, req) + if rec.Code != test.wantStatus { + t.Fatalf("%s: status %d, want %d: %s", instanceType, rec.Code, test.wantStatus, rec.Body.String()) + } + if test.wantPlain && !bytes.Equal(rec.Body.Bytes(), data) { + t.Fatal("ordinary SSE-C GET did not return plaintext") + } + if test.wantCipher && (len(rec.Body.Bytes()) == 0 || bytes.Equal(rec.Body.Bytes(), data)) { + t.Fatal("trusted replication GET did not return ciphertext") + } + }) + } +} + +func TestReplicationTrustControlsInternalOptionsAndEvents(t *testing.T) { + mtime := time.Date(2026, 8, 31, 12, 34, 56, 123, time.UTC) + headers := make(http.Header) + headers.Set(xhttp.MinIOSourceReplicationRequest, "true") + headers.Set(xhttp.MinIOSourceETag, "source-etag") + headers.Set(xhttp.MinIOSourceMTime, mtime.Format(time.RFC3339Nano)) + headers.Set(xhttp.MinIOReplicationActualObjectSize, "123") + headers.Set(ReplicationSsecChecksumHeader, "checksum") + + ordinary, err := putOptsFromHeaders(t.Context(), headers, nil, false) + if err != nil { + t.Fatal(err) + } + if ordinary.ReplicationRequest || ordinary.PreserveETag != "" || !ordinary.MTime.IsZero() { + t.Fatalf("ordinary options trusted internal headers: %#v", ordinary) + } + + trusted, err := putOptsFromHeaders(t.Context(), headers, nil, true) + if err != nil { + t.Fatal(err) + } + if !trusted.ReplicationRequest || trusted.PreserveETag != "source-etag" || !trusted.MTime.Equal(mtime) { + t.Fatalf("trusted options lost source state: %#v", trusted) + } + + completeReq := &http.Request{Header: headers.Clone(), Form: make(url.Values)} + ordinaryComplete, err := completeMultipartOpts(t.Context(), completeReq, "bucket", "object") + if err != nil { + t.Fatal(err) + } + if ordinaryComplete.ReplicationRequest || len(ordinaryComplete.UserDefined) != 0 { + t.Fatalf("ordinary completion trusted internal metadata: %#v", ordinaryComplete) + } + trustedCompleteCtx := withReplicationTrust(t.Context(), true, false) + trustedComplete, err := completeMultipartOpts(trustedCompleteCtx, completeReq.WithContext(trustedCompleteCtx), "bucket", "object") + if err != nil { + t.Fatal(err) + } + if !trustedComplete.ReplicationRequest || trustedComplete.UserDefined[ReservedMetadataPrefix+"Actual-Object-Size"] != "123" || + trustedComplete.UserDefined[ReplicationSsecChecksumHeader] != "checksum" { + t.Fatalf("trusted completion lost internal metadata: %#v", trustedComplete) + } + + req := &http.Request{Header: headers.Clone(), Form: make(url.Values)} + req = req.WithContext(context.Background()) + if _, ok := extractReqParams(req)[xhttp.MinIOSourceReplicationRequest]; ok { + t.Fatal("untrusted marker suppressed events") + } + trustedCtx := withReplicationTrust(req.Context(), true, false) + req = req.WithContext(trustedCtx) + if _, ok := extractReqParams(req)[xhttp.MinIOSourceReplicationRequest]; !ok { + t.Fatal("trusted replication marker was not propagated to events") + } +} + +func TestAPIPutObjectReplicationTrust(t *testing.T) { + defer DetectTestLeak(t)() + ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{ + t: t, + objAPITest: testAPIPutObjectReplicationTrust, + }) +} + +func testAPIPutObjectReplicationTrust(obj ObjectLayer, instanceType, bucketName string, + apiRouter http.Handler, _ auth.Credentials, t *testing.T, +) { + putOnly := newObjectAttributesAuthzUser(t, instanceType, bucketName, `"s3:PutObject"`) + replicator := newObjectAttributesAuthzUser(t, instanceType, bucketName, `"s3:PutObject","s3:ReplicateObject"`) + payload := []byte("replication trust put payload") + sourceMTime := time.Date(2024, 1, 2, 3, 4, 5, 6, time.UTC) + + request := func(t *testing.T, object string, creds auth.Credentials, status string, check bool) *httptest.ResponseRecorder { + t.Helper() + headers := map[string]string{ + xhttp.MinIOSourceReplicationRequest: "true", + xhttp.MinIOSourceETag: "source-etag", + xhttp.MinIOSourceMTime: sourceMTime.Format(time.RFC3339Nano), + } + if status != "" { + headers[xhttp.AmzBucketReplicationStatus] = status + } + if check { + headers[xhttp.MinIOSourceReplicationCheck] = "true" + } + req, err := newTestSignedRequestV4(http.MethodPut, getPutObjectURL("", bucketName, object), + int64(len(payload)), bytes.NewReader(payload), creds.AccessKey, creds.SecretKey, headers) + if err != nil { + t.Fatal(err) + } + rec := httptest.NewRecorder() + apiRouter.ServeHTTP(rec, req) + return rec + } + + t.Run("untrusted marker is ordinary", func(t *testing.T) { + object := "replication-trust/put-ordinary" + if rec := request(t, object, putOnly, "PENDING", false); rec.Code != http.StatusOK { + t.Fatalf("status %d: %s", rec.Code, rec.Body.String()) + } + info, err := obj.GetObjectInfo(t.Context(), bucketName, object, ObjectOptions{}) + if err != nil { + t.Fatal(err) + } + if info.ETag == "source-etag" || info.ModTime.Equal(sourceMTime) { + t.Fatalf("untrusted source state was preserved: ETag=%q MTime=%v", info.ETag, info.ModTime) + } + assertObjectMetadataKeysAbsent(t, info.UserDefined, xhttp.AmzBucketReplicationStatus) + }) + + t.Run("unauthorized replica is denied", func(t *testing.T) { + object := "replication-trust/put-denied-replica" + if rec := request(t, object, putOnly, "REPLICA", false); rec.Code != http.StatusForbidden { + t.Fatalf("status %d, want 403: %s", rec.Code, rec.Body.String()) + } + if _, err := obj.GetObjectInfo(t.Context(), bucketName, object, ObjectOptions{}); err == nil { + t.Fatal("unauthorized replica write created an object") + } + }) + + t.Run("trusted batch preserves source state", func(t *testing.T) { + object := "replication-trust/put-batch" + if rec := request(t, object, replicator, "", false); rec.Code != http.StatusOK { + t.Fatalf("status %d: %s", rec.Code, rec.Body.String()) + } + info, err := obj.GetObjectInfo(t.Context(), bucketName, object, ObjectOptions{}) + if err != nil { + t.Fatal(err) + } + if info.ETag != "source-etag" || !info.ModTime.Equal(sourceMTime) { + t.Fatalf("trusted source state lost: ETag=%q MTime=%v", info.ETag, info.ModTime) + } + assertObjectMetadataKeysAbsent(t, info.UserDefined, xhttp.AmzBucketReplicationStatus) + }) + + t.Run("trusted replica persists replica state", func(t *testing.T) { + object := "replication-trust/put-replica" + if rec := request(t, object, replicator, "REPLICA", false); rec.Code != http.StatusOK { + t.Fatalf("status %d: %s", rec.Code, rec.Body.String()) + } + info, err := obj.GetObjectInfo(t.Context(), bucketName, object, ObjectOptions{}) + if err != nil { + t.Fatal(err) + } + if info.UserDefined[xhttp.AmzBucketReplicationStatus] != "REPLICA" { + t.Fatalf("replica status not persisted: %#v", info.UserDefined) + } + }) + + for _, test := range []struct { + name string + creds auth.Credentials + wantStatus int + }{ + {name: "validity check requires ReplicateObject", creds: putOnly, wantStatus: http.StatusForbidden}, + {name: "validity check succeeds for replicator", creds: replicator, wantStatus: http.StatusBadRequest}, + } { + t.Run(test.name, func(t *testing.T) { + object := "replication-trust/put-check-" + strconv.Itoa(test.wantStatus) + if rec := request(t, object, test.creds, "REPLICA", true); rec.Code != test.wantStatus { + t.Fatalf("status %d, want %d: %s", rec.Code, test.wantStatus, rec.Body.String()) + } + if _, err := obj.GetObjectInfo(t.Context(), bucketName, object, ObjectOptions{}); err == nil { + t.Fatal("replication validity check created an object") + } + }) + } +} + +func TestAPISnowballReplicationTrustIsPerEntry(t *testing.T) { + defer DetectTestLeak(t)() + ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{ + t: t, + objAPITest: testAPISnowballReplicationTrustIsPerEntry, + }) +} + +func testAPISnowballReplicationTrustIsPerEntry(obj ObjectLayer, instanceType, bucketName string, + apiRouter http.Handler, _ auth.Credentials, t *testing.T, +) { + const ( + allowedPrefix = "snowball/allowed/" + deniedPrefix = "snowball/denied/" + sourceETag = "0123456789abcdef0123456789abcdef" + ) + creds := newSnowballReplicationTrustUser(t, instanceType, bucketName, allowedPrefix) + + var body bytes.Buffer + tw := tar.NewWriter(&body) + objects := make([]struct { + name string + trusted bool + }, 0, 32) + for i := 0; i < 16; i++ { + for _, entry := range []struct { + prefix string + trusted bool + }{ + {prefix: allowedPrefix, trusted: true}, + {prefix: deniedPrefix}, + } { + name := entry.prefix + strconv.Itoa(i) + data := []byte("snowball replication trust " + name) + if err := tw.WriteHeader(&tar.Header{Name: name, Mode: 0o600, Size: int64(len(data))}); err != nil { + t.Fatal(err) + } + if _, err := tw.Write(data); err != nil { + t.Fatal(err) + } + objects = append(objects, struct { + name string + trusted bool + }{name: name, trusted: entry.trusted}) + } + } + if err := tw.Close(); err != nil { + t.Fatal(err) + } + + headers := map[string]string{ + xhttp.AmzSnowballExtract: "true", + xhttp.MinIOSourceReplicationRequest: "true", + xhttp.MinIOSourceETag: sourceETag, + } + for _, test := range []struct { + name string + trailer bool + }{ + {name: "signed-v4"}, + {name: "streaming-unsigned-trailer", trailer: true}, + } { + t.Run(test.name, func(t *testing.T) { + var req *http.Request + var err error + if test.trailer { + req, err = newStreamingUnsignedTrailerRequest(http.MethodPut, + getPutObjectURL("", bucketName, "snowball.tar"), body.Bytes(), UTCNow()) + if err == nil { + for name, value := range headers { + req.Header.Set(name, value) + } + err = signRequestV4(req, creds.AccessKey, creds.SecretKey) + } + } else { + req, err = newTestSignedRequestV4(http.MethodPut, getPutObjectURL("", bucketName, "snowball.tar"), + int64(body.Len()), bytes.NewReader(body.Bytes()), creds.AccessKey, creds.SecretKey, headers) + } + if err != nil { + t.Fatal(err) + } + rec := httptest.NewRecorder() + apiRouter.ServeHTTP(rec, req) + if rec.Code != http.StatusOK { + t.Fatalf("%s: Snowball PUT status %d: %s", instanceType, rec.Code, rec.Body.String()) + } + + for _, object := range objects { + info, err := obj.GetObjectInfo(t.Context(), bucketName, object.name, ObjectOptions{}) + if err != nil { + t.Fatalf("%s: get %s: %v", instanceType, object.name, err) + } + if object.trusted && info.ETag != sourceETag { + t.Errorf("%s: trusted entry %s ETag = %q, want source ETag", instanceType, object.name, info.ETag) + } + if !object.trusted && info.ETag == sourceETag { + t.Errorf("%s: untrusted entry %s preserved source ETag", instanceType, object.name) + } + } + }) + } +} + +func TestAPISnowballInheritsBucketEncryption(t *testing.T) { + defer DetectTestLeak(t)() + ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{ + t: t, + objAPITest: testAPISnowballInheritsBucketEncryption, + }) +} + +func testAPISnowballInheritsBucketEncryption(obj ObjectLayer, instanceType, bucketName string, + apiRouter http.Handler, credentials auth.Credentials, t *testing.T, +) { + previousKMS := GlobalKMS + GlobalKMS = kms.NewStub("snowball-default-encryption") + defer func() { GlobalKMS = previousKMS }() + sseXML := []byte(`AES256`) + if _, err := globalBucketMetadataSys.Update(t.Context(), bucketName, bucketSSEConfig, sseXML); err != nil { + t.Fatalf("%s: configure bucket encryption: %v", instanceType, err) + } + + var body bytes.Buffer + tw := tar.NewWriter(&body) + objects := []string{"encrypted/one", "encrypted/two"} + for _, object := range objects { + data := []byte("snowball bucket encryption " + object) + if err := tw.WriteHeader(&tar.Header{Name: object, Mode: 0o600, Size: int64(len(data))}); err != nil { + t.Fatal(err) + } + if _, err := tw.Write(data); err != nil { + t.Fatal(err) + } + } + if err := tw.Close(); err != nil { + t.Fatal(err) + } + + req, err := newTestSignedRequestV4(http.MethodPut, getPutObjectURL("", bucketName, "encrypted-snowball.tar"), + int64(body.Len()), bytes.NewReader(body.Bytes()), credentials.AccessKey, credentials.SecretKey, + map[string]string{xhttp.AmzSnowballExtract: "true"}) + if err != nil { + t.Fatal(err) + } + rec := httptest.NewRecorder() + apiRouter.ServeHTTP(rec, req) + if rec.Code != http.StatusOK { + t.Fatalf("%s: Snowball PUT status %d: %s", instanceType, rec.Code, rec.Body.String()) + } + for _, object := range objects { + info, err := obj.GetObjectInfo(t.Context(), bucketName, object, ObjectOptions{}) + if err != nil { + t.Fatalf("%s: get %s: %v", instanceType, object, err) + } + if _, encrypted := crypto.IsEncrypted(info.UserDefined); !encrypted { + t.Errorf("%s: extracted entry %s did not inherit bucket encryption", instanceType, object) + } + } +} + +func newSnowballReplicationTrustUser(t *testing.T, instanceType, bucketName, allowedPrefix string) auth.Credentials { + t.Helper() + ctx := t.Context() + accessKey, secretKey, err := auth.GenerateCredentials() + if err != nil { + t.Fatalf("%s: generate credentials: %v", instanceType, err) + } + creds := auth.Credentials{AccessKey: accessKey, SecretKey: secretKey} + if _, err = globalIAMSys.CreateUser(ctx, creds.AccessKey, madmin.AddOrUpdateUserReq{ + SecretKey: creds.SecretKey, + Status: madmin.AccountEnabled, + }); err != nil { + t.Fatalf("%s: create Snowball user: %v", instanceType, err) + } + + policyJSON := `{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": ["s3:PutObject"], + "Resource": ["arn:aws:s3:::` + bucketName + `/*"] + }, + { + "Effect": "Allow", + "Action": ["s3:ReplicateObject"], + "Resource": ["arn:aws:s3:::` + bucketName + `/` + allowedPrefix + `*"] + } + ] +}` + parsed, err := policy.ParseConfig(strings.NewReader(policyJSON)) + if err != nil { + t.Fatalf("%s: parse Snowball policy: %v", instanceType, err) + } + policyName := "snowball-replication-trust-" + mustGetUUID() + if _, err = globalIAMSys.SetPolicy(ctx, policyName, *parsed); err != nil { + t.Fatalf("%s: install Snowball policy: %v", instanceType, err) + } + if _, err = globalIAMSys.PolicyDBSet(ctx, creds.AccessKey, policyName, regUser, false); err != nil { + t.Fatalf("%s: attach Snowball policy: %v", instanceType, err) + } + return creds +} + +func TestAPICopyObjectMarkerOnlyDoesNotCopyCiphertext(t *testing.T) { + defer DetectTestLeak(t)() + ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{ + t: t, + objAPITest: testAPICopyObjectMarkerOnlyDoesNotCopyCiphertext, + }) +} + +func testAPICopyObjectMarkerOnlyDoesNotCopyCiphertext(obj ObjectLayer, instanceType, bucketName string, + apiRouter http.Handler, credentials auth.Credentials, t *testing.T, +) { + previousTLS := globalIsTLS + globalIsTLS = true + defer func() { globalIsTLS = previousTLS }() + + key := bytes.Repeat([]byte{0x57}, 32) + keyMD5 := md5.Sum(key) + data := bytes.Repeat([]byte("copy marker-only plaintext "), 256) + sseHeaders := map[string]string{ + xhttp.AmzServerSideEncryptionCustomerAlgorithm: xhttp.AmzEncryptionAES, + xhttp.AmzServerSideEncryptionCustomerKey: base64.StdEncoding.EncodeToString(key), + xhttp.AmzServerSideEncryptionCustomerKeyMD5: base64.StdEncoding.EncodeToString(keyMD5[:]), + } + srcObject := "replication-trust/copy-ssec-source" + dstObject := "replication-trust/copy-marker-only" + putCopyChecksumSource(t, apiRouter, credentials, bucketName, srcObject, data, sseHeaders) + + replicator := newObjectAttributesAuthzUser(t, instanceType, bucketName, + `"s3:GetObject","s3:PutObject","s3:ReplicateObject"`) + headers := map[string]string{ + xhttp.AmzCopySource: url.QueryEscape(SlashSeparator + bucketName + SlashSeparator + srcObject), + xhttp.MinIOSourceReplicationRequest: "true", + xhttp.AmzServerSideEncryptionCopyCustomerAlgorithm: xhttp.AmzEncryptionAES, + xhttp.AmzServerSideEncryptionCopyCustomerKey: base64.StdEncoding.EncodeToString(key), + xhttp.AmzServerSideEncryptionCopyCustomerKeyMD5: base64.StdEncoding.EncodeToString(keyMD5[:]), + } + req, err := newTestSignedRequestV4(http.MethodPut, getCopyObjectURL("", bucketName, dstObject), 0, nil, + replicator.AccessKey, replicator.SecretKey, headers) + if err != nil { + t.Fatal(err) + } + rec := httptest.NewRecorder() + apiRouter.ServeHTTP(rec, req) + if rec.Code != http.StatusOK { + t.Fatalf("%s: CopyObject status %d: %s", instanceType, rec.Code, rec.Body.String()) + } + assertObjectContents(t, obj, bucketName, dstObject, data) +} + +func TestAPIDeleteObjectReplicationTrust(t *testing.T) { + defer DetectTestLeak(t)() + ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{ + t: t, + objAPITest: testAPIDeleteObjectReplicationTrust, + }) +} + +func testAPIDeleteObjectReplicationTrust(obj ObjectLayer, instanceType, bucketName string, + apiRouter http.Handler, _ auth.Credentials, t *testing.T, +) { + deleteOnly := newObjectAttributesAuthzUser(t, instanceType, bucketName, `"s3:DeleteObject"`) + replicator := newObjectAttributesAuthzUser(t, instanceType, bucketName, `"s3:DeleteObject","s3:ReplicateDelete"`) + payload := []byte("delete replication trust") + + put := func(t *testing.T, object string) { + t.Helper() + if _, err := obj.PutObject(t.Context(), bucketName, object, + mustGetPutObjReader(t, bytes.NewReader(payload), int64(len(payload)), "", ""), ObjectOptions{}); err != nil { + t.Fatal(err) + } + } + remove := func(t *testing.T, object string, creds auth.Credentials, versionID string, deleteMarker, check bool) *httptest.ResponseRecorder { + t.Helper() + headers := map[string]string{ + xhttp.MinIOSourceReplicationRequest: "true", + xhttp.AmzBucketReplicationStatus: "REPLICA", + } + if deleteMarker { + headers[xhttp.MinIOSourceDeleteMarker] = "true" + } + if check { + headers[xhttp.MinIOSourceReplicationCheck] = "true" + } + target := getDeleteObjectURL("", bucketName, object) + if versionID != "" { + target += "?" + url.Values{xhttp.VersionID: {versionID}}.Encode() + } + req, err := newTestSignedRequestV4(http.MethodDelete, target, + 0, nil, creds.AccessKey, creds.SecretKey, headers) + if err != nil { + t.Fatal(err) + } + rec := httptest.NewRecorder() + apiRouter.ServeHTTP(rec, req) + return rec + } + + t.Run("replica status without ReplicateDelete is denied", func(t *testing.T) { + object := "replication-trust/delete-denied" + put(t, object) + if rec := remove(t, object, deleteOnly, "", false, false); rec.Code != http.StatusForbidden { + t.Fatalf("status %d, want 403: %s", rec.Code, rec.Body.String()) + } + if _, err := obj.GetObjectInfo(t.Context(), bucketName, object, ObjectOptions{}); err != nil { + t.Fatalf("denied delete removed object: %v", err) + } + }) + + t.Run("trusted replica delete remains supported", func(t *testing.T) { + object := "replication-trust/delete-allowed" + put(t, object) + if rec := remove(t, object, replicator, "", false, false); rec.Code != http.StatusNoContent { + t.Fatalf("status %d, want 204: %s", rec.Code, rec.Body.String()) + } + }) + + for _, shape := range []struct { + name string + deleteMarker bool + }{ + {name: "delete-marker", deleteMarker: true}, + {name: "version-purge"}, + } { + t.Run("validity check/"+shape.name, func(t *testing.T) { + for _, test := range []struct { + name string + creds auth.Credentials + wantStatus int + }{ + {name: "requires ReplicateDelete", creds: deleteOnly, wantStatus: http.StatusForbidden}, + {name: "succeeds for replicator", creds: replicator, wantStatus: http.StatusBadRequest}, + } { + t.Run(test.name, func(t *testing.T) { + object := "replication-trust/delete-check-" + shape.name + "-" + strconv.Itoa(test.wantStatus) + put(t, object) + if rec := remove(t, object, test.creds, mustGetUUID(), shape.deleteMarker, true); rec.Code != test.wantStatus { + t.Fatalf("status %d, want %d: %s", rec.Code, test.wantStatus, rec.Body.String()) + } + if _, err := obj.GetObjectInfo(t.Context(), bucketName, object, ObjectOptions{}); err != nil { + t.Fatalf("replication validity check removed object: %v", err) + } + }) + } + }) + } +} + +func TestAPISSECMultipartReplicationTrust(t *testing.T) { + defer DetectTestLeak(t)() + ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{ + t: t, + objAPITest: testAPISSECMultipartReplicationTrust, + }) +} + +func testAPISSECMultipartReplicationTrust(obj ObjectLayer, instanceType, bucketName string, + apiRouter http.Handler, credentials auth.Credentials, t *testing.T, +) { + previousTLS := globalIsTLS + globalIsTLS = true + defer func() { globalIsTLS = previousTLS }() + + replicator := newObjectAttributesAuthzUser(t, instanceType, bucketName, `"s3:PutObject","s3:GetObject","s3:ReplicateObject"`) + putOnly := newObjectAttributesAuthzUser(t, instanceType, bucketName, `"s3:PutObject"`) + key := bytes.Repeat([]byte{0x42}, 32) + keyMD5 := md5.Sum(key) + data := bytes.Repeat([]byte("trusted multipart replication "), 4096) + sseHeaders := map[string]string{ + xhttp.AmzServerSideEncryptionCustomerAlgorithm: xhttp.AmzEncryptionAES, + xhttp.AmzServerSideEncryptionCustomerKey: base64.StdEncoding.EncodeToString(key), + xhttp.AmzServerSideEncryptionCustomerKeyMD5: base64.StdEncoding.EncodeToString(keyMD5[:]), + } + + // A marker alone must not let an ordinary writer upload raw bytes into an + // SSE-C multipart upload without presenting the customer key. + fakeObject := "replication-trust/ssec-multipart-fake" + fakeNewReq, err := newTestSignedRequestV4(http.MethodPost, getNewMultipartURL("", bucketName, fakeObject), + 0, nil, credentials.AccessKey, credentials.SecretKey, sseHeaders) + if err != nil { + t.Fatal(err) + } + fakeNewRec := httptest.NewRecorder() + apiRouter.ServeHTTP(fakeNewRec, fakeNewReq) + if fakeNewRec.Code != http.StatusOK { + t.Fatalf("fake-path NewMultipart status %d: %s", fakeNewRec.Code, fakeNewRec.Body.String()) + } + var fakeInit InitiateMultipartUploadResponse + if err = xmlDecoder(fakeNewRec.Body, &fakeInit, int64(fakeNewRec.Body.Len())); err != nil { + t.Fatal(err) + } + fakePartReq, err := newTestSignedRequestV4(http.MethodPut, + getPutObjectPartURL("", bucketName, fakeObject, fakeInit.UploadID, "1"), int64(len(data)), bytes.NewReader(data), + putOnly.AccessKey, putOnly.SecretKey, map[string]string{xhttp.MinIOSourceReplicationRequest: "true"}) + if err != nil { + t.Fatal(err) + } + fakePartRec := httptest.NewRecorder() + apiRouter.ServeHTTP(fakePartRec, fakePartReq) + if fakePartRec.Code != http.StatusBadRequest { + t.Fatalf("fake marker PutPart status %d, want 400: %s", fakePartRec.Code, fakePartRec.Body.String()) + } + + object := "replication-trust/ssec-multipart" + + // Create the source as a real SSE-C multipart object so the encrypted part + // layout and metadata match what the replication worker reads. + newReq, err := newTestSignedRequestV4(http.MethodPost, getNewMultipartURL("", bucketName, object), + 0, nil, credentials.AccessKey, credentials.SecretKey, sseHeaders) + if err != nil { + t.Fatal(err) + } + newRec := httptest.NewRecorder() + apiRouter.ServeHTTP(newRec, newReq) + if newRec.Code != http.StatusOK { + t.Fatalf("source NewMultipart status %d: %s", newRec.Code, newRec.Body.String()) + } + var sourceInit InitiateMultipartUploadResponse + if err = xmlDecoder(newRec.Body, &sourceInit, int64(newRec.Body.Len())); err != nil { + t.Fatal(err) + } + + partReq, err := newTestSignedRequestV4(http.MethodPut, + getPutObjectPartURL("", bucketName, object, sourceInit.UploadID, "1"), int64(len(data)), bytes.NewReader(data), + credentials.AccessKey, credentials.SecretKey, sseHeaders) + if err != nil { + t.Fatal(err) + } + partRec := httptest.NewRecorder() + apiRouter.ServeHTTP(partRec, partReq) + if partRec.Code != http.StatusOK { + t.Fatalf("source PutPart status %d: %s", partRec.Code, partRec.Body.String()) + } + sourcePartETag := canonicalizeETag(partRec.Header()[xhttp.ETag][0]) + sourceCompleteBody, err := xml.Marshal(CompleteMultipartUpload{Parts: []CompletePart{{PartNumber: 1, ETag: sourcePartETag}}}) + if err != nil { + t.Fatal(err) + } + completeReq, err := newTestSignedRequestV4(http.MethodPost, + getCompleteMultipartUploadURL("", bucketName, object, sourceInit.UploadID), int64(len(sourceCompleteBody)), + bytes.NewReader(sourceCompleteBody), credentials.AccessKey, credentials.SecretKey, sseHeaders) + if err != nil { + t.Fatal(err) + } + completeRec := httptest.NewRecorder() + apiRouter.ServeHTTP(completeRec, completeReq) + if completeRec.Code != http.StatusOK { + t.Fatalf("source Complete status %d: %s", completeRec.Code, completeRec.Body.String()) + } + + gr, err := obj.GetObjectNInfo(t.Context(), bucketName, object, nil, http.Header{}, ObjectOptions{ReplicationRequest: true}) + if err != nil { + t.Fatal(err) + } + sourceInfo := gr.ObjInfo + rawPart, err := io.ReadAll(gr) + gr.Close() + if err != nil { + t.Fatal(err) + } + if len(rawPart) == 0 || bytes.Equal(rawPart, data) { + t.Fatal("source replication read did not return encrypted bytes") + } + + replicationOpts, isMP, err := putReplicationOpts(t.Context(), "", sourceInfo) + if err != nil { + t.Fatal(err) + } + if !isMP { + t.Fatal("SSE-C multipart source was not recognized as multipart") + } + replicationOpts.Internal.SourceMTime = time.Time{} + replicationHeaders := make(map[string]string) + for name, values := range replicationOpts.Header() { + if len(values) > 0 { + replicationHeaders[name] = values[0] + } + } + + // Start the destination upload over the same key. The existing object stays + // readable until Complete, so buffering rawPart above mirrors a remote peer. + replNewReq, err := newTestSignedRequestV4(http.MethodPost, getNewMultipartURL("", bucketName, object), + 0, nil, replicator.AccessKey, replicator.SecretKey, replicationHeaders) + if err != nil { + t.Fatal(err) + } + replNewRec := httptest.NewRecorder() + apiRouter.ServeHTTP(replNewRec, replNewReq) + if replNewRec.Code != http.StatusOK { + t.Fatalf("replica NewMultipart status %d: %s", replNewRec.Code, replNewRec.Body.String()) + } + var replicaInit InitiateMultipartUploadResponse + if err = xmlDecoder(replNewRec.Body, &replicaInit, int64(replNewRec.Body.Len())); err != nil { + t.Fatal(err) + } + + replPartHeaders := map[string]string{xhttp.MinIOSourceReplicationRequest: "true"} + replPartReq, err := newTestSignedRequestV4(http.MethodPut, + getPutObjectPartURL("", bucketName, object, replicaInit.UploadID, "1"), int64(len(rawPart)), bytes.NewReader(rawPart), + replicator.AccessKey, replicator.SecretKey, replPartHeaders) + if err != nil { + t.Fatal(err) + } + replPartRec := httptest.NewRecorder() + apiRouter.ServeHTTP(replPartRec, replPartReq) + if replPartRec.Code != http.StatusOK { + t.Fatalf("replica PutPart status %d: %s", replPartRec.Code, replPartRec.Body.String()) + } + replPartETag := canonicalizeETag(replPartRec.Header()[xhttp.ETag][0]) + replCompleteBody, err := xml.Marshal(CompleteMultipartUpload{Parts: []CompletePart{{PartNumber: 1, ETag: replPartETag}}}) + if err != nil { + t.Fatal(err) + } + actualSize, err := sourceInfo.GetActualSize() + if err != nil { + t.Fatal(err) + } + replCompleteHeaders := map[string]string{ + xhttp.MinIOSourceReplicationRequest: "true", + xhttp.MinIOSourceMTime: sourceInfo.ModTime.Format(time.RFC3339Nano), + xhttp.MinIOSourceETag: sourceInfo.ETag, + xhttp.MinIOReplicationActualObjectSize: strconv.FormatInt(actualSize, 10), + } + replCompleteReq, err := newTestSignedRequestV4(http.MethodPost, + getCompleteMultipartUploadURL("", bucketName, object, replicaInit.UploadID), int64(len(replCompleteBody)), + bytes.NewReader(replCompleteBody), replicator.AccessKey, replicator.SecretKey, replCompleteHeaders) + if err != nil { + t.Fatal(err) + } + replCompleteRec := httptest.NewRecorder() + apiRouter.ServeHTTP(replCompleteRec, replCompleteReq) + if replCompleteRec.Code != http.StatusOK { + t.Fatalf("replica Complete status %d: %s", replCompleteRec.Code, replCompleteRec.Body.String()) + } + + getReq, err := newTestSignedRequestV4(http.MethodGet, getGetObjectURL("", bucketName, object), + 0, nil, credentials.AccessKey, credentials.SecretKey, sseHeaders) + if err != nil { + t.Fatal(err) + } + getRec := httptest.NewRecorder() + apiRouter.ServeHTTP(getRec, getReq) + if getRec.Code != http.StatusOK { + t.Fatalf("GET replicated object status %d: %s", getRec.Code, getRec.Body.String()) + } + if !bytes.Equal(getRec.Body.Bytes(), data) { + t.Fatal("replicated SSE-C multipart object did not decrypt to source plaintext") + } + + // A replicated SSE-C part is written as raw ciphertext, so its stored + // ActualSize is the ciphertext length. GetObjectAttributes must still + // report the part's plaintext length, which for this single-part object + // is the whole object size. + attributes := attributesPartsFetch(t, apiRouter, credentials, bucketName, object, sseHeaders) + if len(attributes.ObjectParts.Parts) != 1 { + t.Fatalf("replica attributes part count %d, want 1", len(attributes.ObjectParts.Parts)) + } + if got := attributes.ObjectParts.Parts[0].Size; got != int64(len(data)) { + t.Errorf("replica attributes part 1 size=%d, want plaintext size %d", got, len(data)) + } + if attributes.ObjectSize != int64(len(data)) { + t.Errorf("replica attributes ObjectSize=%d, want %d", attributes.ObjectSize, len(data)) + } + if attributes.ObjectParts.Parts[0].Size != attributes.ObjectSize { + t.Errorf("replica attributes part 1 size=%d does not match ObjectSize=%d", + attributes.ObjectParts.Parts[0].Size, attributes.ObjectSize) + } +} + +// 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)) + } +} + +// TestAPICopyObjectReplicaLegalHoldTimestamp verifies that a replicated legal +// hold update records its own timestamp under the legal-hold key: a replica +// that arrives later with an older timestamp must not change the hold, the +// retention timestamp must stay untouched, and a newer replica still applies. +func TestAPICopyObjectReplicaLegalHoldTimestamp(t *testing.T) { + defer DetectTestLeak(t)() + ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{ + t: t, + objAPITest: testAPICopyObjectReplicaLegalHoldTimestamp, + makeBucketOptions: MakeBucketOptions{LockEnabled: true}, + }) +} + +func testAPICopyObjectReplicaLegalHoldTimestamp(obj ObjectLayer, instanceType, bucketName string, + apiRouter http.Handler, _ auth.Credentials, t *testing.T, +) { + object := "replication-trust/legal-hold" + if _, err := obj.PutObject(t.Context(), bucketName, object, mustGetPutObjReader(t, bytes.NewReader([]byte("held")), 4, "", ""), ObjectOptions{}); err != nil { + t.Fatal(err) + } + replicator := newObjectAttributesAuthzUser(t, instanceType, bucketName, + `"s3:GetObject","s3:PutObject","s3:ReplicateObject","s3:PutObjectLegalHold","s3:GetObjectLegalHold","s3:GetObjectRetention"`) + apply := func(status, stamp string) { + t.Helper() + headers := map[string]string{ + xhttp.AmzCopySource: url.QueryEscape(SlashSeparator + bucketName + SlashSeparator + object), + xhttp.AmzMetadataDirective: replaceDirective, + xhttp.MinIOSourceReplicationRequest: "true", + xhttp.AmzBucketReplicationStatus: "REPLICA", + xhttp.AmzObjectLockLegalHold: status, + xhttp.MinIOSourceObjectLegalHoldTimestamp: stamp, + } + req, err := newTestSignedRequestV4(http.MethodPut, getCopyObjectURL("", bucketName, object), 0, nil, + replicator.AccessKey, replicator.SecretKey, headers) + if err != nil { + t.Fatal(err) + } + rec := httptest.NewRecorder() + apiRouter.ServeHTTP(rec, req) + if rec.Code != http.StatusOK { + t.Fatalf("%s: replica CopyObject legal hold %s @ %s: status %d: %s", instanceType, status, stamp, rec.Code, rec.Body.String()) + } + } + state := func() (hold, holdStamp string, hasRetentionStamp bool) { + t.Helper() + info, err := obj.GetObjectInfo(t.Context(), bucketName, object, ObjectOptions{}) + if err != nil { + t.Fatal(err) + } + _, hasRetentionStamp = info.UserDefined[ReservedMetadataPrefixLower+ObjectLockRetentionTimestamp] + return info.UserDefined[strings.ToLower(xhttp.AmzObjectLockLegalHold)], info.UserDefined[ReservedMetadataPrefixLower+ObjectLockLegalHoldTimestamp], hasRetentionStamp + } + + apply("ON", "2026-09-03T10:00:00Z") + apply("OFF", "2026-09-03T09:00:00Z") // stale replica: must be ignored + if hold, stamp, retention := state(); hold != "ON" || stamp != "2026-09-03T10:00:00Z" || retention { + t.Fatalf("%s: after stale OFF: hold=%q legal-hold timestamp=%q retention timestamp present=%v", instanceType, hold, stamp, retention) + } + apply("OFF", "2026-09-03T11:00:00Z") // newer replica: applies + if hold, stamp, retention := state(); hold != "OFF" || stamp != "2026-09-03T11:00:00Z" || retention { + t.Fatalf("%s: after newer OFF: hold=%q legal-hold timestamp=%q retention timestamp present=%v", instanceType, hold, stamp, retention) + } +} + +// Object Lock replication ordering fixtures. A replicated lock update carries +// the source value plus the reserved timestamp that orders it; a removal is an +// update that carries the ordering timestamp and no value. +const ( + objectLockTestRetainUntil = "2030-01-01T00:00:00Z" + objectLockTestStamp0900 = "2026-09-03T09:00:00Z" + objectLockTestStamp0930 = "2026-09-03T09:30:00Z" + objectLockTestStamp1000 = "2026-09-03T10:00:00Z" + objectLockTestStamp1100 = "2026-09-03T11:00:00Z" +) + +// objectLockFields is the Object Lock state stored on an object version, +// including the reserved timestamps that order replicated updates. +type objectLockFields struct { + mode, retainUntil, retentionStamp string + legalHold, legalHoldStamp string +} + +// putObjectLockVersion seeds a versioned object carrying meta and returns its +// version id. +func putObjectLockVersion(t *testing.T, obj ObjectLayer, bucket, object string, meta map[string]string) string { + t.Helper() + info, err := obj.PutObject(t.Context(), bucket, object, + mustGetPutObjReader(t, bytes.NewReader([]byte("data")), 4, "", ""), + ObjectOptions{Versioned: true, UserDefined: meta}) + if err != nil { + t.Fatal(err) + } + return info.VersionID +} + +// readObjectLockFields returns the Object Lock state stored on a version. +func readObjectLockFields(t *testing.T, obj ObjectLayer, bucket, object, versionID string) objectLockFields { + t.Helper() + info, err := obj.GetObjectInfo(t.Context(), bucket, object, ObjectOptions{VersionID: versionID}) + if err != nil { + t.Fatal(err) + } + return objectLockFields{ + mode: info.UserDefined[strings.ToLower(xhttp.AmzObjectLockMode)], + retainUntil: info.UserDefined[strings.ToLower(xhttp.AmzObjectLockRetainUntilDate)], + retentionStamp: info.UserDefined[ReservedMetadataPrefixLower+ObjectLockRetentionTimestamp], + legalHold: info.UserDefined[strings.ToLower(xhttp.AmzObjectLockLegalHold)], + legalHoldStamp: info.UserDefined[ReservedMetadataPrefixLower+ObjectLockLegalHoldTimestamp], + } +} + +// sendReplicaLockCopy issues the signed CopyObject a replication sender emits +// for a metadata update: the version copied onto itself with the REPLACE +// tagging directive, the trusted replication headers, and extra on top. It +// fails the test unless every empty-valued header survived onto the wire and +// the handler accepted the request. +func sendReplicaLockCopy(t *testing.T, apiRouter http.Handler, cred auth.Credentials, bucket, object, versionID string, extra map[string]string) { + t.Helper() + headers := map[string]string{ + xhttp.AmzCopySource: url.QueryEscape(SlashSeparator+bucket+SlashSeparator+object) + "?versionId=" + versionID, + xhttp.AmzTagDirective: replaceDirective, + xhttp.MinIOSourceReplicationRequest: "true", + xhttp.AmzBucketReplicationStatus: "REPLICA", + } + for key, value := range extra { + headers[key] = value + } + req, err := newTestSignedRequestV4(http.MethodPut, getCopyObjectURL("", bucket, object)+"?versionId="+versionID, 0, nil, + cred.AccessKey, cred.SecretKey, headers) + if err != nil { + t.Fatal(err) + } + for key, value := range headers { + if _, ok := req.Header[http.CanonicalHeaderKey(key)]; value == "" && !ok { + t.Fatalf("empty %s header was dropped before the handler", key) + } + } + rec := httptest.NewRecorder() + apiRouter.ServeHTTP(rec, req) + if rec.Code != http.StatusOK { + t.Fatalf("replica CopyObject: status %d: %s", rec.Code, rec.Body.String()) + } +} + +// TestAPICopyObjectReplicaAbsentLockFieldsPreserveNewerState verifies that a +// replica CopyObject carrying no Object Lock values leaves the stored retention +// and legal hold alone when it does not win: its source retention timestamp is +// older than the stored one, and it carries no legal-hold update at all. A +// missing value is not on its own an instruction to erase. +func TestAPICopyObjectReplicaAbsentLockFieldsPreserveNewerState(t *testing.T) { + defer DetectTestLeak(t)() + ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{ + t: t, + objAPITest: testAPICopyObjectReplicaAbsentLockFieldsPreserveNewerState, + makeBucketOptions: MakeBucketOptions{LockEnabled: true}, + }) +} + +func testAPICopyObjectReplicaAbsentLockFieldsPreserveNewerState(obj ObjectLayer, instanceType, bucketName string, + apiRouter http.Handler, cred auth.Credentials, t *testing.T, +) { + object := "replication-trust/lock-absent-fields" + versionID := putObjectLockVersion(t, obj, bucketName, object, map[string]string{ + strings.ToLower(xhttp.AmzObjectLockMode): "GOVERNANCE", + strings.ToLower(xhttp.AmzObjectLockRetainUntilDate): objectLockTestRetainUntil, + ReservedMetadataPrefixLower + ObjectLockRetentionTimestamp: objectLockTestStamp1000, + strings.ToLower(xhttp.AmzObjectLockLegalHold): "ON", + ReservedMetadataPrefixLower + ObjectLockLegalHoldTimestamp: objectLockTestStamp1000, + }) + + // A tag-only replica update: stale retention ordering, and no legal-hold + // update at all. + sendReplicaLockCopy(t, apiRouter, cred, bucketName, object, versionID, map[string]string{ + xhttp.AmzObjectTagging: "application=independent-tag-update", + xhttp.MinIOSourceTaggingTimestamp: objectLockTestStamp1100, + xhttp.MinIOSourceObjectRetentionTimestamp: objectLockTestStamp0930, + }) + + want := objectLockFields{ + mode: "GOVERNANCE", retainUntil: objectLockTestRetainUntil, retentionStamp: objectLockTestStamp1000, + legalHold: "ON", legalHoldStamp: objectLockTestStamp1000, + } + if got := readObjectLockFields(t, obj, bucketName, object, versionID); got != want { + t.Errorf("%s: replica update without Object Lock values changed stored state: got %+v, want %+v", instanceType, got, want) + } +} + +// TestAPICopyObjectReplicaRetentionRemovalKeepsOrderingTimestamp verifies that +// a replicated retention removal both applies and records its own ordering +// timestamp, so a retained update that arrives later with an older timestamp +// cannot resurrect the retention it removed. +func TestAPICopyObjectReplicaRetentionRemovalKeepsOrderingTimestamp(t *testing.T) { + defer DetectTestLeak(t)() + ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{ + t: t, + objAPITest: testAPICopyObjectReplicaRetentionRemovalKeepsOrderingTimestamp, + makeBucketOptions: MakeBucketOptions{LockEnabled: true}, + }) +} + +func testAPICopyObjectReplicaRetentionRemovalKeepsOrderingTimestamp(obj ObjectLayer, instanceType, bucketName string, + apiRouter http.Handler, cred auth.Credentials, t *testing.T, +) { + object := "replication-trust/lock-retention-removal" + versionID := putObjectLockVersion(t, obj, bucketName, object, map[string]string{ + strings.ToLower(xhttp.AmzObjectLockMode): "GOVERNANCE", + strings.ToLower(xhttp.AmzObjectLockRetainUntilDate): objectLockTestRetainUntil, + ReservedMetadataPrefixLower + ObjectLockRetentionTimestamp: objectLockTestStamp0900, + }) + + // The removal is newer than the stored retention, so it applies. + sendReplicaLockCopy(t, apiRouter, cred, bucketName, object, versionID, map[string]string{ + xhttp.MinIOSourceObjectRetentionTimestamp: objectLockTestStamp1000, + }) + want := objectLockFields{retentionStamp: objectLockTestStamp1000} + if got := readObjectLockFields(t, obj, bucketName, object, versionID); got != want { + t.Errorf("%s: after replica retention removal: got %+v, want %+v", instanceType, got, want) + } + + // A retained update that arrives afterwards with an older source timestamp + // must lose, and the removal timestamp must survive the rejection. + sendReplicaLockCopy(t, apiRouter, cred, bucketName, object, versionID, map[string]string{ + xhttp.AmzObjectLockMode: "GOVERNANCE", + xhttp.AmzObjectLockRetainUntilDate: objectLockTestRetainUntil, + xhttp.MinIOSourceObjectRetentionTimestamp: objectLockTestStamp0930, + }) + if got := readObjectLockFields(t, obj, bucketName, object, versionID); got != want { + t.Errorf("%s: after stale retained replica update: got %+v, want %+v", instanceType, got, want) + } +} + +// TestAPICopyObjectReplicaObjectLockOrdering covers the replica lock shapes the +// two regression tests above do not reach: the present-but-empty retention pair +// a sender emits after a retention removal, the REPLACE metadata directive +// under which only the restore helpers can carry stored timestamps forward, and +// an orphaned legal-hold timestamp arriving without a status. +func TestAPICopyObjectReplicaObjectLockOrdering(t *testing.T) { + defer DetectTestLeak(t)() + ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{ + t: t, + objAPITest: testAPICopyObjectReplicaObjectLockOrdering, + makeBucketOptions: MakeBucketOptions{LockEnabled: true}, + }) +} + +func testAPICopyObjectReplicaObjectLockOrdering(obj ObjectLayer, instanceType, bucketName string, + apiRouter http.Handler, cred auth.Credentials, t *testing.T, +) { + retained := func(stamp string) map[string]string { + return map[string]string{ + strings.ToLower(xhttp.AmzObjectLockMode): "GOVERNANCE", + strings.ToLower(xhttp.AmzObjectLockRetainUntilDate): objectLockTestRetainUntil, + ReservedMetadataPrefixLower + ObjectLockRetentionTimestamp: stamp, + } + } + // The shape PutObjectRetention leaves behind after a removal: the public + // keys present but empty, with the ordering timestamp of the removal. + removedUnderHold := map[string]string{ + strings.ToLower(xhttp.AmzObjectLockMode): "", + strings.ToLower(xhttp.AmzObjectLockRetainUntilDate): "", + ReservedMetadataPrefixLower + ObjectLockRetentionTimestamp: objectLockTestStamp1000, + strings.ToLower(xhttp.AmzObjectLockLegalHold): "ON", + ReservedMetadataPrefixLower + ObjectLockLegalHoldTimestamp: objectLockTestStamp1000, + } + // A stale retained update carrying an orphaned newer legal-hold timestamp. + staleRetentionOrphanedHold := map[string]string{ + xhttp.AmzObjectLockMode: "GOVERNANCE", + xhttp.AmzObjectLockRetainUntilDate: objectLockTestRetainUntil, + xhttp.MinIOSourceObjectRetentionTimestamp: objectLockTestStamp0930, + xhttp.MinIOSourceObjectLegalHoldTimestamp: objectLockTestStamp1100, + } + + testCases := []struct { + name string + stored map[string]string + headers map[string]string + replaceMetadata bool + want objectLockFields + }{ + { + // A newer present-empty removal applies and keeps its timestamp, + // exactly as the absent-header shape does. + name: "present-empty-retention-newer", + stored: retained(objectLockTestStamp0900), + headers: map[string]string{ + xhttp.AmzObjectLockMode: "", + xhttp.AmzObjectLockRetainUntilDate: "", + xhttp.MinIOSourceObjectRetentionTimestamp: objectLockTestStamp1000, + }, + want: objectLockFields{retentionStamp: objectLockTestStamp1000}, + }, + { + // The same removal arriving late must not erase newer retention. + name: "present-empty-retention-stale", + stored: retained(objectLockTestStamp1000), + headers: map[string]string{ + xhttp.AmzObjectLockMode: "", + xhttp.AmzObjectLockRetainUntilDate: "", + xhttp.MinIOSourceObjectRetentionTimestamp: objectLockTestStamp0930, + }, + want: objectLockFields{ + mode: "GOVERNANCE", retainUntil: objectLockTestRetainUntil, retentionStamp: objectLockTestStamp1000, + }, + }, + { + // REPLACE rebuilds the metadata from the request headers, which + // never carry the reserved timestamps, so the restore helpers are + // the only thing that can keep the removal timestamp and the hold. + name: "replace-directive-restores-stored-state", + stored: removedUnderHold, + headers: staleRetentionOrphanedHold, + replaceMetadata: true, + want: objectLockFields{ + retentionStamp: objectLockTestStamp1000, + legalHold: "ON", legalHoldStamp: objectLockTestStamp1000, + }, + }, + { + // Under COPY the reserved timestamps ride along on their own, but + // an orphaned legal-hold timestamp still carries no status and must + // not clear the stored hold. + name: "copy-directive-orphaned-legal-hold-timestamp", + stored: removedUnderHold, + headers: staleRetentionOrphanedHold, + want: objectLockFields{ + retentionStamp: objectLockTestStamp1000, + legalHold: "ON", legalHoldStamp: objectLockTestStamp1000, + }, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + object := "replication-trust/lock-ordering/" + testCase.name + versionID := putObjectLockVersion(t, obj, bucketName, object, testCase.stored) + headers := testCase.headers + if testCase.replaceMetadata { + headers = make(map[string]string, len(testCase.headers)+1) + for key, value := range testCase.headers { + headers[key] = value + } + headers[xhttp.AmzMetadataDirective] = replaceDirective + } + sendReplicaLockCopy(t, apiRouter, cred, bucketName, object, versionID, headers) + if got := readObjectLockFields(t, obj, bucketName, object, versionID); got != testCase.want { + t.Fatalf("%s: got %+v, want %+v", instanceType, got, testCase.want) + } + }) + } +} + +// TestAPICopyObjectReplicaRetentionRemovalUnderBucketKMS verifies that the +// replication ordering timestamps reach the Object Lock decision when the +// destination bucket applies default SSE-KMS. That path builds its own +// ObjectOptions, and dropping the timestamps there would leave every +// replicated lock update unordered and silently restore the stored value. +func TestAPICopyObjectReplicaRetentionRemovalUnderBucketKMS(t *testing.T) { + defer DetectTestLeak(t)() + ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{ + t: t, + objAPITest: testAPICopyObjectReplicaRetentionRemovalUnderBucketKMS, + makeBucketOptions: MakeBucketOptions{LockEnabled: true}, + }) +} + +func testAPICopyObjectReplicaRetentionRemovalUnderBucketKMS(obj ObjectLayer, instanceType, bucketName string, + apiRouter http.Handler, cred auth.Credentials, t *testing.T, +) { + previousKMS := GlobalKMS + GlobalKMS = kms.NewStub("object-lock-replication") + defer func() { GlobalKMS = previousKMS }() + sseXML := []byte(`aws:kmsobject-lock-replication`) + if _, err := globalBucketMetadataSys.Update(t.Context(), bucketName, bucketSSEConfig, sseXML); err != nil { + t.Fatalf("%s: configure bucket encryption: %v", instanceType, err) + } + + object := "replication-trust/lock-kms-removal" + versionID := putObjectLockVersion(t, obj, bucketName, object, map[string]string{ + strings.ToLower(xhttp.AmzObjectLockMode): "GOVERNANCE", + strings.ToLower(xhttp.AmzObjectLockRetainUntilDate): objectLockTestRetainUntil, + ReservedMetadataPrefixLower + ObjectLockRetentionTimestamp: objectLockTestStamp0900, + }) + sendReplicaLockCopy(t, apiRouter, cred, bucketName, object, versionID, map[string]string{ + xhttp.MinIOSourceObjectRetentionTimestamp: objectLockTestStamp1000, + }) + + want := objectLockFields{retentionStamp: objectLockTestStamp1000} + if got := readObjectLockFields(t, obj, bucketName, object, versionID); got != want { + t.Errorf("%s: replica retention removal into an SSE-KMS bucket: got %+v, want %+v", instanceType, got, want) + } +} + +// ssecKeyHeaders returns the SSE-C request headers for key, either as the +// destination key or as the copy-source key. +func ssecKeyHeaders(key []byte, copySource bool) map[string]string { + sum := md5.Sum(key) + encoded, digest := base64.StdEncoding.EncodeToString(key), base64.StdEncoding.EncodeToString(sum[:]) + if copySource { + return map[string]string{ + xhttp.AmzServerSideEncryptionCopyCustomerAlgorithm: xhttp.AmzEncryptionAES, + xhttp.AmzServerSideEncryptionCopyCustomerKey: encoded, + xhttp.AmzServerSideEncryptionCopyCustomerKeyMD5: digest, + } + } + return map[string]string{ + xhttp.AmzServerSideEncryptionCustomerAlgorithm: xhttp.AmzEncryptionAES, + xhttp.AmzServerSideEncryptionCustomerKey: encoded, + xhttp.AmzServerSideEncryptionCustomerKeyMD5: digest, + } +} + +// TestAPICopyObjectReplicaLockTimestampSurvivesSSECKeyRotation verifies that a +// replicated Object Lock decision survives an in-place SSE-C key rotation. That +// path snapshots the stored reserved metadata before the decision is made and +// merges it back afterwards to preserve the encryption headers, which would +// otherwise reinstate the ordering timestamp the decision replaced and let a +// stale retained update resurrect a removed retention. +func TestAPICopyObjectReplicaLockTimestampSurvivesSSECKeyRotation(t *testing.T) { + defer DetectTestLeak(t)() + ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{ + t: t, + objAPITest: testAPICopyObjectReplicaLockTimestampSurvivesSSECKeyRotation, + makeBucketOptions: MakeBucketOptions{LockEnabled: true}, + }) +} + +func testAPICopyObjectReplicaLockTimestampSurvivesSSECKeyRotation(obj ObjectLayer, instanceType, bucketName string, + apiRouter http.Handler, cred auth.Credentials, t *testing.T, +) { + previousTLS := globalIsTLS + globalIsTLS = true + defer func() { globalIsTLS = previousTLS }() + + keyA, keyB := bytes.Repeat([]byte{0x11}, 32), bytes.Repeat([]byte{0x22}, 32) + keyC, keyD := bytes.Repeat([]byte{0x33}, 32), bytes.Repeat([]byte{0x44}, 32) + object := "replication-trust/lock-ssec-rotation" + putCopyChecksumSource(t, apiRouter, cred, bucketName, object, + bytes.Repeat([]byte("object lock ssec rotation "), 16), ssecKeyHeaders(keyA, false)) + info, err := obj.GetObjectInfo(t.Context(), bucketName, object, ObjectOptions{}) + if err != nil { + t.Fatal(err) + } + versionID := info.VersionID + + rotate := func(oldKey, newKey []byte, extra map[string]string) { + t.Helper() + headers := ssecKeyHeaders(oldKey, true) + for key, value := range ssecKeyHeaders(newKey, false) { + headers[key] = value + } + for key, value := range extra { + headers[key] = value + } + sendReplicaLockCopy(t, apiRouter, cred, bucketName, object, versionID, headers) + } + + // Replicate a retention, then its removal, each during a key rotation. + rotate(keyA, keyB, map[string]string{ + xhttp.AmzObjectLockMode: "GOVERNANCE", + xhttp.AmzObjectLockRetainUntilDate: objectLockTestRetainUntil, + xhttp.MinIOSourceObjectRetentionTimestamp: objectLockTestStamp1000, + }) + rotate(keyB, keyC, map[string]string{ + xhttp.MinIOSourceObjectRetentionTimestamp: objectLockTestStamp1100, + }) + want := objectLockFields{retentionStamp: objectLockTestStamp1100} + if got := readObjectLockFields(t, obj, bucketName, object, versionID); got != want { + t.Errorf("%s: after replicated removal during key rotation: got %+v, want %+v", instanceType, got, want) + } + + // The stale retained update that follows must still lose the comparison. + rotate(keyC, keyD, map[string]string{ + xhttp.AmzObjectLockMode: "GOVERNANCE", + xhttp.AmzObjectLockRetainUntilDate: objectLockTestRetainUntil, + xhttp.MinIOSourceObjectRetentionTimestamp: "2026-09-03T10:30:00Z", + }) + if got := readObjectLockFields(t, obj, bucketName, object, versionID); got != want { + t.Errorf("%s: after stale retained replay during key rotation: got %+v, want %+v", instanceType, got, want) + } +} + +// TestAPICopyObjectMarkerOnlyLeavesObjectLockUnchanged verifies that the +// the new value-less handling reaches only an actual replica. A trusted peer that +// sends the replication marker without REPLICA status is not replicating lock +// state, so a REPLACE copy carrying no lock headers must write a version with +// no retention and no legal hold, exactly as it did before. +func TestAPICopyObjectMarkerOnlyLeavesObjectLockUnchanged(t *testing.T) { + defer DetectTestLeak(t)() + ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{ + t: t, + objAPITest: testAPICopyObjectMarkerOnlyLeavesObjectLockUnchanged, + makeBucketOptions: MakeBucketOptions{LockEnabled: true}, + }) +} + +func testAPICopyObjectMarkerOnlyLeavesObjectLockUnchanged(obj ObjectLayer, instanceType, bucketName string, + apiRouter http.Handler, cred auth.Credentials, t *testing.T, +) { + object := "replication-trust/lock-marker-only" + putObjectLockVersion(t, obj, bucketName, object, map[string]string{ + strings.ToLower(xhttp.AmzObjectLockMode): "GOVERNANCE", + strings.ToLower(xhttp.AmzObjectLockRetainUntilDate): objectLockTestRetainUntil, + ReservedMetadataPrefixLower + ObjectLockRetentionTimestamp: objectLockTestStamp1000, + strings.ToLower(xhttp.AmzObjectLockLegalHold): "ON", + ReservedMetadataPrefixLower + ObjectLockLegalHoldTimestamp: objectLockTestStamp1000, + }) + + // The replication marker without REPLICA status: trusted, but not a replica. + req, err := newTestSignedRequestV4(http.MethodPut, getCopyObjectURL("", bucketName, object), 0, nil, + cred.AccessKey, cred.SecretKey, map[string]string{ + xhttp.AmzCopySource: url.QueryEscape(SlashSeparator + bucketName + SlashSeparator + object), + xhttp.AmzMetadataDirective: replaceDirective, + xhttp.MinIOSourceReplicationRequest: "true", + }) + if err != nil { + t.Fatal(err) + } + rec := httptest.NewRecorder() + apiRouter.ServeHTTP(rec, req) + if rec.Code != http.StatusOK { + t.Fatalf("%s: marker-only CopyObject: status %d: %s", instanceType, rec.Code, rec.Body.String()) + } + + if got := readObjectLockFields(t, obj, bucketName, object, ""); got != (objectLockFields{}) { + t.Errorf("%s: marker-only copy inherited Object Lock state: got %+v, want none", instanceType, got) + } +} diff --git a/cmd/s3-zip-handlers.go b/cmd/s3-zip-handlers.go index e2c91226c..b7fe426bf 100644 --- a/cmd/s3-zip-handlers.go +++ b/cmd/s3-zip-handlers.go @@ -32,8 +32,8 @@ import ( "github.com/minio/minio/internal/crypto" xhttp "github.com/minio/minio/internal/http" xioutil "github.com/minio/minio/internal/ioutil" - "github.com/minio/pkg/v3/policy" "github.com/minio/zipindex" + "github.com/pgsty/silo-pkg/v3/policy" ) const ( diff --git a/cmd/server-main.go b/cmd/server-main.go index 60b1ce29d..8a93d2d55 100644 --- a/cmd/server-main.go +++ b/cmd/server-main.go @@ -48,13 +48,14 @@ import ( "github.com/minio/minio/internal/color" "github.com/minio/minio/internal/config" "github.com/minio/minio/internal/config/api" + "github.com/minio/minio/internal/config/notify" "github.com/minio/minio/internal/handlers" "github.com/minio/minio/internal/hash/sha256" xhttp "github.com/minio/minio/internal/http" xioutil "github.com/minio/minio/internal/ioutil" "github.com/minio/minio/internal/logger" - "github.com/minio/pkg/v3/certs" - "github.com/minio/pkg/v3/env" + "github.com/pgsty/silo-pkg/v3/certs" + "github.com/pgsty/silo-pkg/v3/env" "gopkg.in/yaml.v2" ) @@ -536,6 +537,15 @@ func configRetriableErrors(err error) bool { notInitialized } +func fatalServerConfigError(err error) bool { + var configErr config.Err + if errors.As(err, &configErr) { + return true + } + var migrationErr *notify.LegacyDatabaseTargetError + return errors.As(err, &migrationErr) +} + func bootstrapTraceMsg(msg string) { info := madmin.TraceInfo{ TraceType: madmin.TraceBootstrap, @@ -633,7 +643,10 @@ func initConfigSubsystem(ctx context.Context, newObject ObjectLayer) error { // Initialize config system. if err := globalConfigSys.Init(newObject); err != nil { - if configRetriableErrors(err) { + var migrationErr *notify.LegacyDatabaseTargetError + // Do not use fatalServerConfigError here: existing config.Err values + // retain the historical log-and-continue behavior at this boundary. + if configRetriableErrors(err) || errors.As(err, &migrationErr) { return fmt.Errorf("Unable to initialize config system: %w", err) } @@ -966,10 +979,9 @@ func serverMain(ctx *cli.Context) { var err error bootstrapTrace("initServerConfig", func() { if err = initServerConfig(GlobalContext, newObject); err != nil { - var cerr config.Err // For any config error, we don't need to drop into safe-mode // instead its a user error and should be fixed by user. - if errors.As(err, &cerr) { + if fatalServerConfigError(err) { logger.FatalIf(err, "Unable to initialize the server") } diff --git a/cmd/server-rlimit.go b/cmd/server-rlimit.go index ecb779e17..3f365853b 100644 --- a/cmd/server-rlimit.go +++ b/cmd/server-rlimit.go @@ -24,7 +24,7 @@ import ( "github.com/dustin/go-humanize" "github.com/minio/madmin-go/v3/kernel" "github.com/minio/minio/internal/logger" - "github.com/minio/pkg/v3/sys" + "github.com/pgsty/silo-pkg/v3/sys" ) func oldLinux() bool { @@ -46,7 +46,9 @@ func oldLinux() bool { func setMaxResources(ctx serverCtxt) (err error) { // Set the Go runtime max threads threshold to 90% of kernel setting. + //nolint:staticcheck // Linux implementations can fail; BSD stubs return a constant nil error. sysMaxThreads, err := sys.GetMaxThreads() + //nolint:staticcheck // Keep the shared cross-platform error handling. if err == nil { minioMaxThreads := (sysMaxThreads * 90) / 100 // Only set max threads if it is greater than the default one diff --git a/cmd/server-startup-msg.go b/cmd/server-startup-msg.go index 51bece519..32cf4a16e 100644 --- a/cmd/server-startup-msg.go +++ b/cmd/server-startup-msg.go @@ -23,7 +23,7 @@ import ( "net/url" "strings" - xnet "github.com/minio/pkg/v3/net" + xnet "github.com/pgsty/silo-pkg/v3/net" "github.com/minio/minio/internal/color" "github.com/minio/minio/internal/logger" diff --git a/cmd/server_test.go b/cmd/server_test.go index f790dfaac..df584014e 100644 --- a/cmd/server_test.go +++ b/cmd/server_test.go @@ -44,7 +44,7 @@ import ( "github.com/minio/minio-go/v7/pkg/set" "github.com/minio/minio-go/v7/pkg/signer" xhttp "github.com/minio/minio/internal/http" - "github.com/minio/pkg/v3/policy" + "github.com/pgsty/silo-pkg/v3/policy" ) // API suite container common to both ErasureSD and Erasure. diff --git a/cmd/sftp-server-driver.go b/cmd/sftp-server-driver.go index 3ce7c0b43..df7f67a57 100644 --- a/cmd/sftp-server-driver.go +++ b/cmd/sftp-server-driver.go @@ -34,7 +34,7 @@ import ( "github.com/minio/minio-go/v7" "github.com/minio/minio-go/v7/pkg/credentials" xioutil "github.com/minio/minio/internal/ioutil" - "github.com/minio/pkg/v3/mimedb" + "github.com/pgsty/silo-pkg/v3/mimedb" "github.com/pkg/sftp" "golang.org/x/crypto/ssh" ) diff --git a/cmd/sftp-server.go b/cmd/sftp-server.go index 0a0164a4b..76f3c6303 100644 --- a/cmd/sftp-server.go +++ b/cmd/sftp-server.go @@ -31,8 +31,8 @@ import ( "github.com/minio/madmin-go/v3" "github.com/minio/minio/internal/auth" "github.com/minio/minio/internal/logger" - xldap "github.com/minio/pkg/v3/ldap" - xsftp "github.com/minio/pkg/v3/sftp" + xldap "github.com/pgsty/silo-pkg/v3/ldap" + xsftp "github.com/pgsty/silo-pkg/v3/sftp" "github.com/pkg/sftp" "golang.org/x/crypto/ssh" ) diff --git a/cmd/signature-v4-utils.go b/cmd/signature-v4-utils.go index 1569dec1c..82b767ed3 100644 --- a/cmd/signature-v4-utils.go +++ b/cmd/signature-v4-utils.go @@ -31,7 +31,7 @@ import ( "github.com/minio/minio/internal/hash/sha256" xhttp "github.com/minio/minio/internal/http" "github.com/minio/minio/internal/logger" - "github.com/minio/pkg/v3/policy" + "github.com/pgsty/silo-pkg/v3/policy" ) // http Header "x-amz-content-sha256" == "UNSIGNED-PAYLOAD" indicates that the diff --git a/cmd/site-replication-bucket-adoption_test.go b/cmd/site-replication-bucket-adoption_test.go new file mode 100644 index 000000000..09f494bd5 --- /dev/null +++ b/cmd/site-replication-bucket-adoption_test.go @@ -0,0 +1,237 @@ +// Copyright (c) 2015-2026 MinIO, Inc. +// +// This file is part of MinIO Object Storage stack +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package cmd + +import ( + "bytes" + "net/http" + "testing" + "time" + + "github.com/minio/minio/internal/auth" +) + +func TestPeerBucketAdoptionPreservesLockAndVersioningConfigs(t *testing.T) { + defer DetectTestLeak(t)() + ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{ + t: t, + objAPITest: testPeerBucketAdoptionPreservesLockAndVersioningConfigs, + makeBucketOptions: MakeBucketOptions{LockEnabled: true}, + }) +} + +func testPeerBucketAdoptionPreservesLockAndVersioningConfigs(_ ObjectLayer, instanceType, bucketName string, + _ http.Handler, _ auth.Credentials, t *testing.T, +) { + objectLockXML := []byte(`EnabledGOVERNANCE30`) + // A locked bucket carries plain Enabled versioning; adoption must keep the + // existing document and its timestamp rather than rewrite them. + versioningXML := []byte(`Enabled`) + if _, err := globalBucketMetadataSys.Update(t.Context(), bucketName, objectLockConfig, objectLockXML); err != nil { + t.Fatal(err) + } + if _, err := globalBucketMetadataSys.Update(t.Context(), bucketName, bucketVersioningConfig, versioningXML); err != nil { + t.Fatal(err) + } + before, err := globalBucketMetadataSys.Get(bucketName) + if err != nil { + t.Fatal(err) + } + + if err := globalSiteReplicationSys.PeerBucketMakeWithVersioningHandler(t.Context(), bucketName, MakeBucketOptions{ + CreatedAt: before.Created.Add(-time.Hour), + LockEnabled: true, + }); err != nil { + t.Fatalf("%s: adopting existing bucket failed: %v", instanceType, err) + } + after, err := globalBucketMetadataSys.Get(bucketName) + if err != nil { + t.Fatal(err) + } + if !bytes.Equal(after.ObjectLockConfigXML, before.ObjectLockConfigXML) || !after.ObjectLockConfigUpdatedAt.Equal(before.ObjectLockConfigUpdatedAt) { + t.Fatalf("%s: Object Lock config changed during adoption", instanceType) + } + if !bytes.Equal(after.VersioningConfigXML, before.VersioningConfigXML) || !after.VersioningConfigUpdatedAt.Equal(before.VersioningConfigUpdatedAt) { + t.Fatalf("%s: versioning config changed during adoption", instanceType) + } +} + +func TestPeerBucketAdoptionBootstrapsMissingConfigs(t *testing.T) { + defer DetectTestLeak(t)() + ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{ + t: t, + objAPITest: testPeerBucketAdoptionBootstrapsMissingConfigs, + }) +} + +func TestPeerBucketAdoptionNormalizesVersioningWhenEnablingLock(t *testing.T) { + defer DetectTestLeak(t)() + ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{ + t: t, + objAPITest: testPeerBucketAdoptionNormalizesVersioningWhenEnablingLock, + }) +} + +func testPeerBucketAdoptionNormalizesVersioningWhenEnablingLock(_ ObjectLayer, instanceType, bucketName string, + _ http.Handler, _ auth.Credentials, t *testing.T, +) { + versioningXML := []byte(`Enabledtruetemporary/`) + if _, err := globalBucketMetadataSys.Update(t.Context(), bucketName, bucketVersioningConfig, versioningXML); err != nil { + t.Fatal(err) + } + before, err := globalBucketMetadataSys.Get(bucketName) + if err != nil { + t.Fatal(err) + } + if err := globalSiteReplicationSys.PeerBucketMakeWithVersioningHandler(t.Context(), bucketName, MakeBucketOptions{ + CreatedAt: before.Created, + LockEnabled: true, + }); err != nil { + t.Fatal(err) + } + after, err := globalBucketMetadataSys.Get(bucketName) + if err != nil { + t.Fatal(err) + } + 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) + } +} + +func TestPeerBucketAdoptionEnablesSuspendedVersioning(t *testing.T) { + defer DetectTestLeak(t)() + ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{ + t: t, + objAPITest: testPeerBucketAdoptionEnablesSuspendedVersioning, + }) +} + +func testPeerBucketAdoptionEnablesSuspendedVersioning(_ ObjectLayer, instanceType, bucketName string, + _ http.Handler, _ auth.Credentials, t *testing.T, +) { + suspended := []byte(`Suspended`) + if _, err := globalBucketMetadataSys.Update(t.Context(), bucketName, bucketVersioningConfig, suspended); err != nil { + t.Fatal(err) + } + before, err := globalBucketMetadataSys.Get(bucketName) + if err != nil { + t.Fatal(err) + } + if err := globalSiteReplicationSys.PeerBucketMakeWithVersioningHandler(t.Context(), bucketName, MakeBucketOptions{CreatedAt: before.Created}); err != nil { + t.Fatal(err) + } + after, err := globalBucketMetadataSys.Get(bucketName) + if err != nil { + t.Fatal(err) + } + if after.versioningConfig == nil || !after.versioningConfig.Enabled() { + t.Fatalf("%s: versioning remained disabled: %q", instanceType, after.VersioningConfigXML) + } + if !after.VersioningConfigUpdatedAt.After(before.VersioningConfigUpdatedAt) { + t.Fatalf("%s: versioning update time = %v, want after %v", instanceType, after.VersioningConfigUpdatedAt, before.VersioningConfigUpdatedAt) + } +} + +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(``) + if err := enablePeerBucketVersioning(&meta, false); err != nil { + t.Fatal(err) + } + if !bytes.Equal(meta.VersioningConfigXML, enabledBucketVersioningConfig) || meta.VersioningConfigUpdatedAt.IsZero() { + t.Fatalf("invalid versioning was not repaired: xml=%q updatedAt=%v", meta.VersioningConfigXML, meta.VersioningConfigUpdatedAt) + } +} + +func testPeerBucketAdoptionBootstrapsMissingConfigs(_ ObjectLayer, instanceType, bucketName string, + _ http.Handler, _ auth.Credentials, t *testing.T, +) { + before, err := globalBucketMetadataSys.Get(bucketName) + if err != nil { + t.Fatal(err) + } + if len(before.ObjectLockConfigXML) != 0 || len(before.VersioningConfigXML) != 0 { + t.Fatalf("%s: invalid bootstrap precondition", instanceType) + } + if err := globalSiteReplicationSys.PeerBucketMakeWithVersioningHandler(t.Context(), bucketName, MakeBucketOptions{ + CreatedAt: before.Created, + LockEnabled: true, + }); err != nil { + t.Fatalf("%s: adopting existing bucket failed: %v", instanceType, err) + } + after, err := globalBucketMetadataSys.Get(bucketName) + if err != nil { + t.Fatal(err) + } + if !bytes.Equal(after.ObjectLockConfigXML, enabledBucketObjectLockConfig) || !bytes.Equal(after.VersioningConfigXML, enabledBucketVersioningConfig) { + t.Fatalf("%s: missing bootstrap configs: objectLock=%q versioning=%q", instanceType, after.ObjectLockConfigXML, after.VersioningConfigXML) + } + if !after.ObjectLockConfigUpdatedAt.Equal(before.Created) || !after.VersioningConfigUpdatedAt.Equal(before.Created) { + t.Fatalf("%s: bootstrap timestamps = (%v, %v), want %v", 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(`EnabledGOVERNANCE30`) + if _, err := globalBucketMetadataSys.Update(t.Context(), bucketName, objectLockConfig, lockWithRule); err != nil { + t.Fatal(err) + } + for name, versioningXML := range map[string][]byte{ + "prefix-excluded": []byte(`Enabledtruetemporary/`), + "suspended": []byte(`Suspended`), + } { + 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) + } + } +} diff --git a/cmd/site-replication-object-lock_test.go b/cmd/site-replication-object-lock_test.go new file mode 100644 index 000000000..bf28df9fa --- /dev/null +++ b/cmd/site-replication-object-lock_test.go @@ -0,0 +1,268 @@ +// Copyright (c) 2015-2026 MinIO, Inc. +// +// This file is part of MinIO Object Storage stack +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package cmd + +import ( + "bytes" + "encoding/base64" + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/minio/madmin-go/v3" + "github.com/minio/minio/internal/auth" + "github.com/minio/mux" +) + +func TestSRBucketObjectLockMetadata(t *testing.T) { + updatedAt := time.Date(2026, time.August, 29, 8, 0, 0, 0, time.UTC) + current := "current" + legacy := "legacy" + + event := newSRBucketObjectLockMeta("bucket", ¤t, updatedAt) + if event.Type != madmin.SRBucketMetaTypeObjectLockConfig || event.Bucket != "bucket" || + event.ObjectLockConfig == nil || *event.ObjectLockConfig != current || event.Tags != nil || !event.UpdatedAt.Equal(updatedAt) { + t.Fatalf("unexpected Object Lock event: %#v", event) + } + + encoded, err := json.Marshal(event) + if err != nil { + t.Fatal(err) + } + var roundTrip madmin.SRBucketMeta + if err := json.Unmarshal(encoded, &roundTrip); err != nil { + t.Fatal(err) + } + if roundTrip.ObjectLockConfig == nil || *roundTrip.ObjectLockConfig != current || roundTrip.Tags != nil { + t.Fatalf("unexpected JSON round trip: %#v", roundTrip) + } + + for _, test := range []struct { + name string + item madmin.SRBucketMeta + want *string + }{ + {name: "current", item: madmin.SRBucketMeta{ObjectLockConfig: ¤t}, want: ¤t}, + {name: "legacy", item: madmin.SRBucketMeta{Tags: &legacy}, want: &legacy}, + {name: "current wins", item: madmin.SRBucketMeta{ObjectLockConfig: ¤t, Tags: &legacy}, want: ¤t}, + {name: "missing", item: madmin.SRBucketMeta{}}, + } { + t.Run(test.name, func(t *testing.T) { + got := srObjectLockPayload(test.item) + if test.want == nil { + if got != nil { + t.Fatalf("payload = %q, want nil", *got) + } + return + } + if got == nil || *got != *test.want { + t.Fatalf("payload = %v, want %q", got, *test.want) + } + }) + } +} + +func TestPeerBucketObjectLockMetadataCurrentAndLegacyPayloads(t *testing.T) { + defer DetectTestLeak(t)() + ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{ + t: t, + objAPITest: testPeerBucketObjectLockMetadataCurrentAndLegacyPayloads, + makeBucketOptions: MakeBucketOptions{LockEnabled: true}, + }) +} + +func applySRBucketMetaViaAdmin(t *testing.T, credentials auth.Credentials, item madmin.SRBucketMeta) *httptest.ResponseRecorder { + t.Helper() + body, err := json.Marshal(item) + if err != nil { + t.Fatal(err) + } + adminRouter := mux.NewRouter() + registerAdminRouter(adminRouter, true) + path := adminPathPrefix + adminAPIVersionPrefix + "/site-replication/peer/bucket-meta" + req, err := newTestSignedRequestV4(http.MethodPut, path, int64(len(body)), bytes.NewReader(body), + credentials.AccessKey, credentials.SecretKey, nil) + if err != nil { + t.Fatal(err) + } + rec := httptest.NewRecorder() + adminRouter.ServeHTTP(rec, req) + return rec +} + +func testPeerBucketObjectLockMetadataCurrentAndLegacyPayloads(_ ObjectLayer, instanceType, bucketName string, + _ http.Handler, credentials auth.Credentials, t *testing.T, +) { + apply := func(item madmin.SRBucketMeta, wantDays uint64) { + t.Helper() + rec := applySRBucketMetaViaAdmin(t, credentials, item) + if rec.Code != http.StatusOK { + t.Fatalf("%s: admin Object Lock apply returned %d: %s", instanceType, rec.Code, rec.Body.String()) + } + config, _, err := globalBucketMetadataSys.GetObjectLockConfig(bucketName) + if err != nil { + t.Fatal(err) + } + if config.Rule == nil || config.Rule.DefaultRetention.Mode != "GOVERNANCE" || + config.Rule.DefaultRetention.Days == nil || *config.Rule.DefaultRetention.Days != wantDays { + t.Fatalf("%s: persisted Object Lock config = %s, want GOVERNANCE/%d days", instanceType, config, wantDays) + } + } + + config30 := base64.StdEncoding.EncodeToString([]byte(`EnabledGOVERNANCE30`)) + apply(newSRBucketObjectLockMeta(bucketName, &config30, UTCNow().Add(time.Hour)), 30) + + config45 := base64.StdEncoding.EncodeToString([]byte(`EnabledGOVERNANCE45`)) + apply(madmin.SRBucketMeta{ + Type: madmin.SRBucketMetaTypeObjectLockConfig, + Bucket: bucketName, + Tags: &config45, + UpdatedAt: UTCNow().Add(2 * time.Hour), + }, 45) +} + +func TestPeerBucketObjectLockMetadataWithoutLockEnabled(t *testing.T) { + defer DetectTestLeak(t)() + ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{ + t: t, + objAPITest: testPeerBucketObjectLockMetadataWithoutLockEnabled, + }) +} + +func testPeerBucketObjectLockMetadataWithoutLockEnabled(_ ObjectLayer, instanceType, bucketName string, + _ http.Handler, credentials auth.Credentials, t *testing.T, +) { + config := base64.StdEncoding.EncodeToString([]byte(`EnabledGOVERNANCE30`)) + item := newSRBucketObjectLockMeta(bucketName, &config, UTCNow().Add(time.Hour)) + rec := applySRBucketMetaViaAdmin(t, credentials, item) + if rec.Code != http.StatusOK { + t.Fatalf("%s: admin Object Lock apply returned %d: %s", instanceType, rec.Code, rec.Body.String()) + } + meta, err := globalBucketMetadataSys.Get(bucketName) + if err != nil { + t.Fatal(err) + } + // 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) + } +} + +func TestHealObjectLockMetadataUsesObjectLockField(t *testing.T) { + defer DetectTestLeak(t)() + ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{ + t: t, + objAPITest: testHealObjectLockMetadataUsesObjectLockField, + }) +} + +func testHealObjectLockMetadataUsesObjectLockField(obj ObjectLayer, instanceType, bucketName string, + _ http.Handler, credentials auth.Credentials, t *testing.T, +) { + ctx := t.Context() + localID := globalDeploymentID() + remoteID := "remote-object-lock-heal" + updatedAt := UTCNow().Add(time.Hour) + createdAt := updatedAt.Add(-time.Hour) + config := base64.StdEncoding.EncodeToString([]byte(`EnabledGOVERNANCE30`)) + + remoteApplies := make(chan madmin.SRBucketMeta, 1) + remote := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + var applied madmin.SRBucketMeta + if err := json.NewDecoder(r.Body).Decode(&applied); err != nil { + t.Errorf("%s: decode remote apply: %v", instanceType, err) + w.WriteHeader(http.StatusBadRequest) + return + } + remoteApplies <- applied + w.WriteHeader(http.StatusOK) + })) + defer remote.Close() + + serviceCred, err := auth.CreateCredentials("object-lock-heal-svc", "object-lock-heal-service-secret") + if err != nil { + t.Fatal(err) + } + serviceCred.ParentUser = credentials.AccessKey + if _, err = globalIAMSys.store.AddServiceAccount(ctx, serviceCred); err != nil { + t.Fatal(err) + } + defer globalIAMSys.DeleteServiceAccount(ctx, serviceCred.AccessKey, false) + + globalSiteReplicationSys.Lock() + oldEnabled := globalSiteReplicationSys.enabled + oldState := globalSiteReplicationSys.state + globalSiteReplicationSys.enabled = true + globalSiteReplicationSys.state = srState{ + Name: "object-lock-heal-test", + ServiceAccountAccessKey: serviceCred.AccessKey, + Peers: map[string]madmin.PeerInfo{ + localID: {Name: "local", DeploymentID: localID}, + remoteID: {Name: "remote", DeploymentID: remoteID, Endpoint: remote.URL}, + }, + } + globalSiteReplicationSys.Unlock() + defer func() { + globalSiteReplicationSys.Lock() + globalSiteReplicationSys.enabled = oldEnabled + globalSiteReplicationSys.state = oldState + globalSiteReplicationSys.Unlock() + }() + + status := srStatusInfo{ + Sites: map[string]madmin.PeerInfo{ + localID: {Name: "local", DeploymentID: localID}, + remoteID: {Name: "remote", DeploymentID: remoteID, Endpoint: remote.URL}, + }, + BucketStats: map[string]map[string]srBucketStatsSummary{ + bucketName: { + localID: { + SRBucketStatsSummary: madmin.SRBucketStatsSummary{OLockConfigMismatch: true}, + meta: srBucketMetaInfo{SRBucketInfo: madmin.SRBucketInfo{ + Bucket: bucketName, + CreatedAt: createdAt, + ObjectLockConfig: &config, + ObjectLockConfigUpdatedAt: updatedAt, + }, DeploymentID: localID}, + }, + remoteID: { + SRBucketStatsSummary: madmin.SRBucketStatsSummary{OLockConfigMismatch: true}, + meta: srBucketMetaInfo{SRBucketInfo: madmin.SRBucketInfo{ + Bucket: bucketName, + CreatedAt: createdAt, + }, DeploymentID: remoteID}, + }, + }, + }, + } + if err := globalSiteReplicationSys.healOLockConfigMetadata(ctx, obj, bucketName, status); err != nil { + t.Fatal(err) + } + select { + case applied := <-remoteApplies: + if applied.Type != madmin.SRBucketMetaTypeObjectLockConfig || applied.Bucket != bucketName || + applied.ObjectLockConfig == nil || *applied.ObjectLockConfig != config || applied.Tags != nil || !applied.UpdatedAt.Equal(updatedAt) { + t.Fatalf("%s: remote heal apply = %#v", instanceType, applied) + } + case <-time.After(5 * time.Second): + t.Fatalf("%s: remote heal did not dispatch Object Lock metadata", instanceType) + } +} diff --git a/cmd/site-replication-status-accounting_test.go b/cmd/site-replication-status-accounting_test.go new file mode 100644 index 000000000..7f5d23e03 --- /dev/null +++ b/cmd/site-replication-status-accounting_test.go @@ -0,0 +1,194 @@ +// Copyright (c) 2015-2026 MinIO, Inc. +// +// This file is part of MinIO Object Storage stack +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package cmd + +import ( + "encoding/base64" + "encoding/json" + "fmt" + "net/http" + "net/http/httptest" + "testing" + + "github.com/minio/madmin-go/v3" + "github.com/minio/minio/internal/auth" +) + +func TestSiteReplicationStatusAccountsPerSiteAndSurvivesMalformedConfig(t *testing.T) { + defer DetectTestLeak(t)() + ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{ + t: t, + objAPITest: testSiteReplicationStatusAccountsPerSiteAndSurvivesMalformedConfig, + }) +} + +func testSiteReplicationStatusAccountsPerSiteAndSurvivesMalformedConfig(obj ObjectLayer, instanceType, localBucket string, + _ http.Handler, credentials auth.Credentials, t *testing.T, +) { + ctx := t.Context() + remoteBucket := getRandomBucketName() + if err := obj.MakeBucket(ctx, remoteBucket, MakeBucketOptions{}); err != nil { + t.Fatal(err) + } + remoteBucketMeta, err := loadBucketMetadata(ctx, obj, remoteBucket) + if err != nil { + t.Fatal(err) + } + globalBucketMetadataSys.Set(remoteBucket, remoteBucketMeta) + globalNotificationSys.LoadBucketMetadata(ctx, remoteBucket) + + tagXML := []byte(`keyvalue`) + versioningXML := []byte(`Enabled`) + objectLockXML := []byte(`EnabledGOVERNANCE30`) + sseXML := []byte(`AES256`) + quotaJSON, err := json.Marshal(madmin.BucketQuota{Type: madmin.HardQuota, Quota: 1024}) + if err != nil { + t.Fatal(err) + } + policyJSON := []byte(fmt.Sprintf(`{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":{"AWS":["*"]},"Action":["s3:GetObject"],"Resource":["arn:aws:s3:::%s/*"]}]}`, localBucket)) + + for configFile, data := range map[string][]byte{ + bucketTaggingConfig: tagXML, + bucketVersioningConfig: versioningXML, + objectLockConfig: objectLockXML, + bucketSSEConfig: sseXML, + bucketQuotaConfigFile: quotaJSON, + bucketPolicyConfig: policyJSON, + } { + 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) + return &encoded + } + remotePolicy := []byte(fmt.Sprintf(`{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":{"AWS":["*"]},"Action":["s3:GetObject"],"Resource":["arn:aws:s3:::%s/*"]}]}`, remoteBucket)) + localMeta, err := globalBucketMetadataSys.Get(localBucket) + if err != nil { + t.Fatal(err) + } + remoteInfo := madmin.SRInfo{ + DeploymentID: "remote-status-accounting", + Buckets: map[string]madmin.SRBucketInfo{ + localBucket: { + Bucket: localBucket, + CreatedAt: localMeta.Created, + }, + remoteBucket: { + Bucket: remoteBucket, + CreatedAt: remoteBucketMeta.Created, + Tags: encode(tagXML), + Versioning: encode(versioningXML), + ObjectLockConfig: encode(objectLockXML), + SSEConfig: encode(sseXML), + QuotaConfig: encode(quotaJSON), + Policy: remotePolicy, + CorsConfig: encode([]byte(testSiteReplicationCORSDoc)), + CorsConfigUpdatedAt: remoteBucketMeta.Created, + }, + }, + } + remote := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(remoteInfo); err != nil { + t.Errorf("%s: encode remote metadata: %v", instanceType, err) + } + })) + defer remote.Close() + + serviceCred, err := auth.CreateCredentials("status-accounting-svc", "status-accounting-service-secret") + if err != nil { + t.Fatal(err) + } + serviceCred.ParentUser = credentials.AccessKey + if _, err = globalIAMSys.store.AddServiceAccount(ctx, serviceCred); err != nil { + t.Fatal(err) + } + defer globalIAMSys.DeleteServiceAccount(ctx, serviceCred.AccessKey, false) + + localID := globalDeploymentID() + remoteID := remoteInfo.DeploymentID + globalSiteReplicationSys.Lock() + oldEnabled := globalSiteReplicationSys.enabled + oldState := globalSiteReplicationSys.state + globalSiteReplicationSys.enabled = true + globalSiteReplicationSys.state = srState{ + Name: "status-accounting-test", + ServiceAccountAccessKey: serviceCred.AccessKey, + Peers: map[string]madmin.PeerInfo{ + localID: {Name: "local", DeploymentID: localID}, + remoteID: {Name: "remote", DeploymentID: remoteID, Endpoint: remote.URL}, + }, + } + globalSiteReplicationSys.Unlock() + defer func() { + globalSiteReplicationSys.Lock() + globalSiteReplicationSys.enabled = oldEnabled + globalSiteReplicationSys.state = oldState + globalSiteReplicationSys.Unlock() + }() + + check := func(name string, wantRemoteTags, wantRemoteQuota int) { + t.Helper() + status, err := globalSiteReplicationSys.siteReplicationStatus(ctx, obj, madmin.SRStatusOptions{Buckets: true}) + if err != nil { + t.Fatal(err) + } + local := status.StatsSummary[localID] + remote := status.StatsSummary[remoteID] + if local.TotalBucketsCount != 2 || remote.TotalBucketsCount != 2 { + t.Fatalf("%s: bucket totals = local:%d remote:%d", name, local.TotalBucketsCount, remote.TotalBucketsCount) + } + if local.TotalTagsCount != 1 || remote.TotalTagsCount != wantRemoteTags || + local.TotalLockConfigCount != 1 || remote.TotalLockConfigCount != 1 || + local.TotalSSEConfigCount != 1 || remote.TotalSSEConfigCount != 1 || + local.TotalVersioningConfigCount != 1 || remote.TotalVersioningConfigCount != 1 || + local.TotalBucketPoliciesCount != 1 || remote.TotalBucketPoliciesCount != 1 || + local.TotalQuotaConfigCount != 1 || remote.TotalQuotaConfigCount != wantRemoteQuota || + local.TotalCorsConfigCount != 1 || remote.TotalCorsConfigCount != 1 { + t.Fatalf("%s: site totals = local:%+v remote:%+v", name, local, remote) + } + if local.ReplicatedTags != 0 || remote.ReplicatedTags != 0 || + local.ReplicatedBucketPolicies != 0 || remote.ReplicatedBucketPolicies != 0 || + local.ReplicatedQuotaConfig != 0 || remote.ReplicatedQuotaConfig != 0 { + t.Fatalf("%s: asymmetric configs counted as replicated: local:%+v remote:%+v", name, local, remote) + } + remoteBucketStatus := status.BucketStats[remoteBucket][remoteID] + if remoteBucketStatus.HasTagsSet != (wantRemoteTags != 0) || remoteBucketStatus.HasQuotaCfgSet != (wantRemoteQuota != 0) { + t.Fatalf("%s: remote bucket presence = tags:%v quota:%v", name, remoteBucketStatus.HasTagsSet, remoteBucketStatus.HasQuotaCfgSet) + } + } + + check("valid asymmetric configs", 1, 1) + invalidTags := "not-base64" + info := remoteInfo.Buckets[remoteBucket] + info.Tags = &invalidTags + remoteInfo.Buckets[remoteBucket] = info + check("malformed tags do not drop site", 0, 1) + + emptyQuota := base64.StdEncoding.EncodeToString([]byte(`{}`)) + info = remoteInfo.Buckets[remoteBucket] + info.QuotaConfig = &emptyQuota + remoteInfo.Buckets[remoteBucket] = info + check("empty quota is absent", 0, 0) +} diff --git a/cmd/site-replication.go b/cmd/site-replication.go index 0b2f26f76..8e9dc7ba8 100644 --- a/cmd/site-replication.go +++ b/cmd/site-replication.go @@ -43,11 +43,13 @@ import ( "github.com/minio/minio-go/v7/pkg/replication" "github.com/minio/minio-go/v7/pkg/set" "github.com/minio/minio/internal/auth" + "github.com/minio/minio/internal/bucket/cors" "github.com/minio/minio/internal/bucket/lifecycle" sreplication "github.com/minio/minio/internal/bucket/replication" + "github.com/minio/minio/internal/bucket/versioning" "github.com/minio/minio/internal/logger" - xldap "github.com/minio/pkg/v3/ldap" - "github.com/minio/pkg/v3/policy" + xldap "github.com/pgsty/silo-pkg/v3/ldap" + "github.com/pgsty/silo-pkg/v3/policy" "github.com/puzpuzpuz/xsync/v3" ) @@ -887,6 +889,36 @@ func (c *SiteReplicationSys) DeleteBucketHook(ctx context.Context, bucket string return errors.Unwrap(cerr) } +// 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() { + meta.VersioningConfigUpdatedAt = meta.Created + } + return nil + } + config, err := versioning.ParseConfig(bytes.NewReader(meta.VersioningConfigXML)) + if err != nil || (lockEnabled && (config.Suspended() || config.PrefixesExcluded())) { + meta.VersioningConfigXML = enabledBucketVersioningConfig + meta.VersioningConfigUpdatedAt = UTCNow() + return nil + } + if config.Enabled() { + return nil + } + config.Status = versioning.Enabled + meta.VersioningConfigXML, err = xml.Marshal(config) + if err != nil { + return err + } + meta.VersioningConfigUpdatedAt = UTCNow() + return nil +} + // PeerBucketMakeWithVersioningHandler - creates bucket and enables versioning. func (c *SiteReplicationSys) PeerBucketMakeWithVersioningHandler(ctx context.Context, bucket string, opts MakeBucketOptions) error { objAPI := newObjectLayerFn() @@ -902,30 +934,34 @@ func (c *SiteReplicationSys) PeerBucketMakeWithVersioningHandler(ctx context.Con if !ok1 && !ok2 { return wrapSRErr(c.annotateErr(makeBucketWithVersion, err)) } - } else { - // Load updated bucket metadata into memory as new - // bucket was created. - globalNotificationSys.LoadBucketMetadata(GlobalContext, bucket) } - - meta, err := globalBucketMetadataSys.Get(bucket) + ctx, unlock, err := lockBucketMetadata(ctx, objAPI, bucket) if err != nil { return wrapSRErr(c.annotateErr(makeBucketWithVersion, err)) } + err = func() error { + defer unlock() + meta, err := loadBucketMetadataParse(ctx, objAPI, bucket, true) + if err != nil { + return err + } + meta.SetCreatedAt(opts.CreatedAt) - meta.SetCreatedAt(opts.CreatedAt) - - meta.VersioningConfigXML = enabledBucketVersioningConfig - if opts.LockEnabled { - meta.ObjectLockConfigXML = enabledBucketObjectLockConfig + if err = enablePeerBucketVersioning(&meta, opts.LockEnabled || len(meta.ObjectLockConfigXML) != 0); err != nil { + return err + } + if opts.LockEnabled && len(meta.ObjectLockConfigXML) == 0 { + meta.ObjectLockConfigXML = enabledBucketObjectLockConfig + if meta.ObjectLockConfigUpdatedAt.IsZero() { + meta.ObjectLockConfigUpdatedAt = meta.Created + } + } + return globalBucketMetadataSys.saveMetadata(bgContext(ctx), objAPI, meta) + }() + if err != nil { + return wrapSRErr(c.annotateErr(makeBucketWithVersion, err)) } - if err := meta.Save(context.Background(), objAPI); err != nil { - return wrapSRErr(err) - } - - globalBucketMetadataSys.Set(bucket, meta) - // Load updated bucket metadata into memory as new metadata updated. globalNotificationSys.LoadBucketMetadata(GlobalContext, bucket) return nil @@ -1577,12 +1613,38 @@ func (c *SiteReplicationSys) PeerBucketMetadataUpdateHandler(ctx context.Context return wrapSRErr(errInvalidArgument) } + var corsConfigData []byte + if item.Cors != nil { + var err error + corsConfigData, err = decodeCORSReplicationPayload(item.Cors) + if err != nil { + return wrapSRErr(err) + } + if err = validateCORSReplicationPayload(corsConfigData); err != nil { + return wrapSRErr(err) + } + } + notifyCtx := ctx + ctx, unlock, err := lockBucketMetadata(ctx, objectAPI, item.Bucket) + if err != nil { + return wrapSRErr(err) + } + locked := true + defer func() { + if locked { + unlock() + } + }() + meta, err := readBucketMetadata(ctx, objectAPI, item.Bucket) if err != nil { return wrapSRErr(err) } if meta.Created.After(item.UpdatedAt) { + if item.Cors != nil { + replLogOnceIf(ctx, fmt.Errorf("ignoring CORS event for bucket %s from %v before bucket creation at %v", item.Bucket, item.UpdatedAt, meta.Created), "cors-event-before-bucket-creation-"+item.Bucket) + } return nil } @@ -1632,7 +1694,22 @@ func (c *SiteReplicationSys) PeerBucketMetadataUpdateHandler(ctx context.Context meta.QuotaConfigUpdatedAt = item.UpdatedAt } - return globalBucketMetadataSys.save(ctx, meta) + if item.Cors != nil { + localState := newCORSReplicationState(meta.CorsConfigXML, meta.CorsConfigUpdatedAt) + incoming := newCORSReplicationState(corsConfigData, item.UpdatedAt) + if compareCORSReplicationStates(localState, incoming) < 0 { + meta.CorsConfigXML = bytes.Clone(corsConfigData) + meta.CorsConfigUpdatedAt = item.UpdatedAt + } + } + + if err = globalBucketMetadataSys.saveMetadata(ctx, objectAPI, meta); err != nil { + return err + } + unlock() + locked = false + globalNotificationSys.LoadBucketMetadata(bgContext(notifyCtx), item.Bucket) + return nil } // PeerBucketPolicyHandler - copies/deletes policy to local cluster. @@ -1696,6 +1773,26 @@ func (c *SiteReplicationSys) PeerBucketTaggingHandler(ctx context.Context, bucke return nil } +func newSRBucketObjectLockMeta(bucket string, config *string, updatedAt time.Time) madmin.SRBucketMeta { + return madmin.SRBucketMeta{ + Type: madmin.SRBucketMetaTypeObjectLockConfig, + Bucket: bucket, + ObjectLockConfig: config, + UpdatedAt: updatedAt, + } +} + +func srObjectLockPayload(item madmin.SRBucketMeta) *string { + if item.ObjectLockConfig != nil { + return item.ObjectLockConfig + } + return item.Tags +} + +func (c *SiteReplicationSys) peerBucketObjectLockConfigItem(ctx context.Context, item madmin.SRBucketMeta) error { + return c.PeerBucketObjectLockConfigHandler(ctx, item.Bucket, srObjectLockPayload(item), item.UpdatedAt) +} + // PeerBucketObjectLockConfigHandler - sets object lock on local bucket. func (c *SiteReplicationSys) PeerBucketObjectLockConfigHandler(ctx context.Context, bucket string, objectLockData *string, updatedAt time.Time) error { if objectLockData != nil { @@ -1749,6 +1846,234 @@ func (c *SiteReplicationSys) PeerBucketSSEConfigHandler(ctx context.Context, buc return nil } +type corsReplicationStateKind uint8 + +const ( + corsReplicationBaseline corsReplicationStateKind = iota + corsReplicationLive + corsReplicationTombstone +) + +type corsReplicationState struct { + kind corsReplicationStateKind + payload []byte + updatedAt time.Time +} + +func newCORSReplicationState(payload []byte, updatedAt time.Time) corsReplicationState { + state := corsReplicationState{updatedAt: updatedAt.UTC()} + switch { + case len(payload) > 0: + state.kind = corsReplicationLive + state.payload = bytes.Clone(payload) + case updatedAt.IsZero(): + state.kind = corsReplicationBaseline + default: + state.kind = corsReplicationTombstone + } + return state +} + +func compareCORSReplicationStates(a, b corsReplicationState) int { + switch { + case a.updatedAt.Before(b.updatedAt): + return -1 + case a.updatedAt.After(b.updatedAt): + return 1 + case a.kind < b.kind: + return -1 + case a.kind > b.kind: + return 1 + case a.kind == corsReplicationLive: + return bytes.Compare(a.payload, b.payload) + default: + return 0 + } +} + +func equalCORSReplicationStates(a, b corsReplicationState) bool { + return compareCORSReplicationStates(a, b) == 0 +} + +func decodeCORSReplicationPayload(encoded *string) ([]byte, error) { + if encoded == nil { + return nil, nil + } + payload, err := base64.StdEncoding.Strict().DecodeString(*encoded) + if err != nil { + return nil, fmt.Errorf("invalid CORS replication payload: %w", err) + } + if len(payload) == 0 || base64.StdEncoding.EncodeToString(payload) != *encoded { + return nil, fmt.Errorf("invalid CORS replication payload: %w", errInvalidArgument) + } + return payload, nil +} + +func validateCORSReplicationPayload(payload []byte) error { + if payload == nil { + return nil + } + config, err := cors.ParseBucketCorsConfig(bytes.NewReader(payload)) + if err != nil { + return fmt.Errorf("invalid CORS replication payload: %w", errInvalidArgument) + } + if err = config.Validate(); err != nil { + return fmt.Errorf("invalid CORS replication payload: %w: %v", errInvalidArgument, err) + } + return nil +} + +func corsReplicationStateFromInfo(info madmin.SRBucketInfo) (corsReplicationState, error) { + payload, err := decodeCORSReplicationPayload(info.CorsConfig) + if err != nil { + return corsReplicationState{}, err + } + if err = validateCORSReplicationPayload(payload); err != nil { + return corsReplicationState{}, err + } + if info.CorsConfig != nil && info.CorsConfigUpdatedAt.IsZero() { + return corsReplicationState{}, fmt.Errorf("live CORS replication payload has no source timestamp: %w", errInvalidArgument) + } + return newCORSReplicationState(payload, info.CorsConfigUpdatedAt), nil +} + +func areCORSReplicationStatesEqual(sites []srBucketMetaInfo) bool { + if len(sites) == 0 { + return true + } + reference, err := corsReplicationStateFromInfo(sites[0].SRBucketInfo) + if err != nil { + return false + } + for _, site := range sites[1:] { + state, err := corsReplicationStateFromInfo(site.SRBucketInfo) + if err != nil || !equalCORSReplicationStates(reference, state) { + return false + } + } + return true +} + +func (s corsReplicationState) encodedPayload() *string { + if s.kind != corsReplicationLive { + return nil + } + encoded := base64.StdEncoding.EncodeToString(s.payload) + return &encoded +} + +func newBucketCORSReplicationEvent(bucket string, meta BucketMetadata) (madmin.SRBucketMeta, bool) { + if meta.CorsConfigUpdatedAt.IsZero() { + return madmin.SRBucketMeta{}, false + } + return madmin.SRBucketMeta{ + Type: madmin.SRBucketMetaTypeCorsConfig, + Bucket: bucket, + Cors: newCORSReplicationState(meta.CorsConfigXML, meta.CorsConfigUpdatedAt).encodedPayload(), + UpdatedAt: meta.CorsConfigUpdatedAt, + }, true +} + +func updateLocalBucketCORSMetadata(ctx context.Context, objectAPI ObjectLayer, bucket string, configData []byte) (time.Time, error) { + return applyBucketCORSMetadata(ctx, objectAPI, bucket, configData, time.Time{}, true) +} + +// localCORSUpdatedAt returns the timestamp a locally originated CORS update +// must carry: now, advanced strictly past both bucket creation and the current +// CORS timestamp. A CORS event stamped before bucket creation is discarded as +// belonging to an older incarnation of the bucket, so a local write must never +// land at or below that floor. +func localCORSUpdatedAt(meta BucketMetadata, now time.Time) time.Time { + updatedAt := now.UTC() + floor := meta.Created + if current := meta.CorsConfigUpdatedAt.UTC(); current.After(floor) { + floor = current + } + if !updatedAt.After(floor) { + updatedAt = floor.Add(time.Nanosecond) + } + return updatedAt +} + +func applyBucketCORSMetadata(ctx context.Context, objectAPI ObjectLayer, bucket string, configData []byte, sourceUpdatedAt time.Time, local bool) (time.Time, error) { + if bucket == "" || (configData != nil && len(configData) == 0) { + return time.Time{}, errInvalidArgument + } + if err := validateCORSReplicationPayload(configData); err != nil { + return time.Time{}, err + } + + notifyCtx := ctx + ctx, unlock, err := lockBucketMetadata(ctx, objectAPI, bucket) + if err != nil { + return time.Time{}, err + } + locked := true + defer func() { + if locked { + unlock() + } + }() + + var meta BucketMetadata + if local { + meta, err = loadBucketMetadataParse(ctx, objectAPI, bucket, true) + } else { + meta, err = readBucketMetadata(ctx, objectAPI, bucket) + } + if err != nil { + return time.Time{}, err + } + + localState := newCORSReplicationState(meta.CorsConfigXML, meta.CorsConfigUpdatedAt) + updatedAt := sourceUpdatedAt.UTC() + if local { + updatedAt = localCORSUpdatedAt(meta, UTCNow()) + } else { + // CreatedAt is the bucket-lineage floor: an event from an older + // incarnation of the bucket must not change the current one. + if updatedAt.Before(meta.Created) { + replLogOnceIf(ctx, fmt.Errorf("ignoring CORS event for bucket %s from %v before bucket creation at %v", bucket, updatedAt, meta.Created), "cors-event-before-bucket-creation-"+bucket) + return localState.updatedAt, nil + } + incoming := newCORSReplicationState(configData, updatedAt) + if compareCORSReplicationStates(localState, incoming) >= 0 { + return localState.updatedAt, nil + } + } + + meta.CorsConfigXML = bytes.Clone(configData) + meta.CorsConfigUpdatedAt = updatedAt + if err = globalBucketMetadataSys.saveMetadata(ctx, objectAPI, meta); err != nil { + return time.Time{}, err + } + unlock() + locked = false + globalNotificationSys.LoadBucketMetadata(bgContext(notifyCtx), bucket) + return updatedAt, nil +} + +// PeerBucketCorsConfigHandler - copies/deletes CORS config to local cluster. +func (c *SiteReplicationSys) PeerBucketCorsConfigHandler(ctx context.Context, bucket string, corsConfig *string, updatedAt time.Time) error { + objectAPI := newObjectLayerFn() + if objectAPI == nil { + return errSRObjectLayerNotReady + } + + if bucket == "" || updatedAt.IsZero() { + return wrapSRErr(errInvalidArgument) + } + + configData, err := decodeCORSReplicationPayload(corsConfig) + if err != nil { + return wrapSRErr(err) + } + if _, err = applyBucketCORSMetadata(ctx, objectAPI, bucket, configData, updatedAt, false); err != nil { + return wrapSRErr(err) + } + return nil +} + // PeerBucketQuotaConfigHandler - copies/deletes policy to local cluster. func (c *SiteReplicationSys) PeerBucketQuotaConfigHandler(ctx context.Context, bucket string, quota *madmin.BucketQuota, updatedAt time.Time) error { // skip overwrite if local update is newer than peer update. @@ -1924,12 +2249,7 @@ func (c *SiteReplicationSys) syncToAllPeers(ctx context.Context, addOpts madmin. objLockCfgData, tm := meta.ObjectLockConfigXML, meta.ObjectLockConfigUpdatedAt if len(objLockCfgData) > 0 { objLockStr := base64.StdEncoding.EncodeToString(objLockCfgData) - err = c.BucketMetaHook(ctx, madmin.SRBucketMeta{ - Type: madmin.SRBucketMetaTypeObjectLockConfig, - Bucket: bucket, - Tags: &objLockStr, - UpdatedAt: tm, - }) + err = c.BucketMetaHook(ctx, newSRBucketObjectLockMeta(bucket, &objLockStr, tm)) if err != nil { return errSRBucketMetaError(err) } @@ -1950,6 +2270,14 @@ func (c *SiteReplicationSys) syncToAllPeers(ctx context.Context, addOpts madmin. } } + // Replicate existing bucket CORS settings + if corsEvent, ok := newBucketCORSReplicationEvent(bucket, meta); ok { + err = c.BucketMetaHook(ctx, corsEvent) + if err != nil { + return errSRBucketMetaError(err) + } + } + // Replicate existing bucket quotas settings quotaConfigJSON, tm := meta.QuotaConfigJSON, meta.QuotaConfigUpdatedAt if len(quotaConfigJSON) > 0 { @@ -2720,6 +3048,7 @@ func (c *SiteReplicationSys) SiteReplicationStatus(ctx context.Context, objAPI O st.VersioningConfigMismatch || st.OLockConfigMismatch || st.SSEConfigMismatch || + st.CorsCfgMismatch || st.PolicyMismatch || st.ReplicationCfgMismatch || st.QuotaCfgMismatch || @@ -3145,77 +3474,117 @@ func (c *SiteReplicationSys) siteReplicationStatus(ctx context.Context, objAPI O quotaCfgs := make([]*madmin.BucketQuota, numSites) sseCfgSet := set.NewStringSet() versionCfgSet := set.NewStringSet() - var tagCount, olockCfgCount, sseCfgCount, versionCfgCount int + validReplCfg := make([]bool, numSites) + validVersionCfg := make([]bool, numSites) + validQuotaCfg := make([]bool, numSites) + validTags := make([]bool, numSites) + validPolicies := make([]bool, numSites) + validObjectLockCfg := make([]bool, numSites) + validSSECfg := make([]bool, numSites) + validCorsCfg := make([]bool, numSites) + var tagCount, olockCfgCount, policyCount, quotaCfgCount, sseCfgCount, corsCfgCount, versionCfgCount int for i, s := range slc { + logInvalid := func(configType string, err error) { + replLogOnceIf(ctx, + fmt.Errorf("unable to parse %s metadata for bucket %s from site %s: %w", configType, b, s.DeploymentID, err), + "site-replication-status-"+configType+"-"+b+"-"+s.DeploymentID) + } if s.ReplicationConfig != nil { cfgBytes, err := base64.StdEncoding.DecodeString(*s.ReplicationConfig) - if err != nil { - continue + if err == nil { + cfg, err := sreplication.ParseConfig(bytes.NewReader(cfgBytes)) + if err == nil { + replCfgs[i] = cfg + validReplCfg[i] = true + } else { + logInvalid("replication", err) + } + } else { + logInvalid("replication", err) } - cfg, err := sreplication.ParseConfig(bytes.NewReader(cfgBytes)) - if err != nil { - continue - } - replCfgs[i] = cfg } if s.Versioning != nil { configData, err := base64.StdEncoding.DecodeString(*s.Versioning) - if err != nil { - continue - } - versionCfgCount++ - if !versionCfgSet.Contains(string(configData)) { - versionCfgSet.Add(string(configData)) + if err == nil { + validVersionCfg[i] = true + versionCfgCount++ + if !versionCfgSet.Contains(string(configData)) { + versionCfgSet.Add(string(configData)) + } + } else { + logInvalid("versioning", err) } } if s.QuotaConfig != nil { cfgBytes, err := base64.StdEncoding.DecodeString(*s.QuotaConfig) - if err != nil { - continue + if err == nil { + cfg, err := parseBucketQuota(b, cfgBytes) + if err == nil { + if cfg != nil && *cfg != (madmin.BucketQuota{}) { + quotaCfgs[i] = cfg + validQuotaCfg[i] = true + quotaCfgCount++ + } + } else { + logInvalid("quota", err) + } + } else { + logInvalid("quota", err) } - cfg, err := parseBucketQuota(b, cfgBytes) - if err != nil { - continue - } - quotaCfgs[i] = cfg } if s.Tags != nil { tagBytes, err := base64.StdEncoding.DecodeString(*s.Tags) - if err != nil { - continue - } - tagCount++ - if !tagSet.Contains(string(tagBytes)) { - tagSet.Add(string(tagBytes)) + if err == nil { + validTags[i] = true + tagCount++ + if !tagSet.Contains(string(tagBytes)) { + tagSet.Add(string(tagBytes)) + } + } else { + logInvalid("tags", err) } } if len(s.Policy) > 0 { plcy, err := policy.ParseBucketPolicyConfig(bytes.NewReader(s.Policy), b) - if err != nil { - continue + if err == nil { + policies[i] = plcy + validPolicies[i] = true + policyCount++ + } else { + logInvalid("policy", err) } - policies[i] = plcy } if s.ObjectLockConfig != nil { configData, err := base64.StdEncoding.DecodeString(*s.ObjectLockConfig) - if err != nil { - continue - } - olockCfgCount++ - if !olockConfigSet.Contains(string(configData)) { - olockConfigSet.Add(string(configData)) + if err == nil { + validObjectLockCfg[i] = true + olockCfgCount++ + if !olockConfigSet.Contains(string(configData)) { + olockConfigSet.Add(string(configData)) + } + } else { + logInvalid("object-lock", err) } } if s.SSEConfig != nil { configData, err := base64.StdEncoding.DecodeString(*s.SSEConfig) - if err != nil { - continue - } - sseCfgCount++ - if !sseCfgSet.Contains(string(configData)) { - sseCfgSet.Add(string(configData)) + if err == nil { + validSSECfg[i] = true + sseCfgCount++ + if !sseCfgSet.Contains(string(configData)) { + sseCfgSet.Add(string(configData)) + } + } else { + logInvalid("sse", err) } } + corsState, err := corsReplicationStateFromInfo(s.SRBucketInfo) + if err != nil { + logInvalid("cors", err) + } else if corsState.kind == corsReplicationLive { + validCorsCfg[i] = true + corsCfgCount++ + } ss, ok := info.StatsSummary[s.DeploymentID] if !ok { ss = madmin.SRSiteSummary{} @@ -3225,26 +3594,33 @@ func (c *SiteReplicationSys) siteReplicationStatus(ctx context.Context, objAPI O ss.ReplicatedBuckets++ } ss.TotalBucketsCount++ - if tagCount > 0 { + if validTags[i] { ss.TotalTagsCount++ } - if olockCfgCount > 0 { + if validObjectLockCfg[i] { ss.TotalLockConfigCount++ } - if sseCfgCount > 0 { + if validSSECfg[i] { ss.TotalSSEConfigCount++ } - if versionCfgCount > 0 { + if validCorsCfg[i] { + ss.TotalCorsConfigCount++ + } + if validVersionCfg[i] { ss.TotalVersioningConfigCount++ } - if len(policies) > 0 { + if validPolicies[i] { ss.TotalBucketPoliciesCount++ } + if validQuotaCfg[i] { + ss.TotalQuotaConfigCount++ + } info.StatsSummary[s.DeploymentID] = ss } tagMismatch := !isReplicated(tagCount, numSites, tagSet) olockCfgMismatch := !isReplicated(olockCfgCount, numSites, olockConfigSet) sseCfgMismatch := !isReplicated(sseCfgCount, numSites, sseCfgSet) + corsCfgMismatch := !areCORSReplicationStatesEqual(slc) versionCfgMismatch := !isReplicated(versionCfgCount, numSites, versionCfgSet) policyMismatch := !isBktPolicyReplicated(numSites, policies) replCfgMismatch := !isBktReplCfgReplicated(numSites, replCfgs) @@ -3267,16 +3643,18 @@ func (c *SiteReplicationSys) siteReplicationStatus(ctx context.Context, objAPI O TagMismatch: tagMismatch, OLockConfigMismatch: olockCfgMismatch, SSEConfigMismatch: sseCfgMismatch, + CorsCfgMismatch: corsCfgMismatch, VersioningConfigMismatch: versionCfgMismatch, PolicyMismatch: policyMismatch, ReplicationCfgMismatch: replCfgMismatch, QuotaCfgMismatch: quotaCfgMismatch, - HasReplicationCfg: s.ReplicationConfig != nil, - HasTagsSet: s.Tags != nil, - HasOLockConfigSet: s.ObjectLockConfig != nil, - HasPolicySet: s.Policy != nil, + HasReplicationCfg: validReplCfg[i], + HasTagsSet: validTags[i], + HasOLockConfigSet: validObjectLockCfg[i], + HasPolicySet: validPolicies[i], HasQuotaCfgSet: quotaCfgSet, - HasSSECfgSet: s.SSEConfig != nil, + HasSSECfgSet: validSSECfg[i], + HasCorsCfgSet: validCorsCfg[i], } var m srBucketMetaInfo if len(bucketStats[s.Bucket]) > dIdx { @@ -3299,12 +3677,18 @@ func (c *SiteReplicationSys) siteReplicationStatus(ctx context.Context, objAPI O if !sseCfgMismatch && sseCfgCount == numSites { sum.ReplicatedSSEConfig++ } - if !policyMismatch && len(policies) == numSites { + if !corsCfgMismatch && corsCfgCount == numSites { + sum.ReplicatedCorsConfig++ + } + if !policyMismatch && policyCount == numSites { sum.ReplicatedBucketPolicies++ } if !tagMismatch && tagCount == numSites { sum.ReplicatedTags++ } + if !quotaCfgMismatch && quotaCfgCount == numSites { + sum.ReplicatedQuotaConfig++ + } info.StatsSummary[s.DeploymentID] = sum } } @@ -3489,7 +3873,16 @@ func isBktQuotaCfgReplicated(total int, quotaCfgs []*madmin.BucketQuota) bool { prev = q continue } - if prev.Quota != q.Quota || prev.Type != q.Type { + if prev.Type != q.Type { + return false + } + if prev.Type == madmin.HardQuota { + if getBucketQuotaSize(prev) != getBucketQuotaSize(q) { + return false + } + continue + } + if prev.Size != q.Size || prev.Quota != q.Quota { return false } } @@ -3709,6 +4102,12 @@ func (c *SiteReplicationSys) SiteReplicationMetaInfo(ctx context.Context, objAPI bms.SSEConfigUpdatedAt = meta.EncryptionConfigUpdatedAt } + bms.CorsConfigUpdatedAt = meta.CorsConfigUpdatedAt + if len(meta.CorsConfigXML) > 0 { + corsConfigStr := base64.StdEncoding.EncodeToString(meta.CorsConfigXML) + bms.CorsConfig = &corsConfigStr + } + if len(meta.ReplicationConfigXML) > 0 { rcfgXMLStr := base64.StdEncoding.EncodeToString(meta.ReplicationConfigXML) bms.ReplicationConfig = &rcfgXMLStr @@ -3734,7 +4133,7 @@ func (c *SiteReplicationSys) SiteReplicationMetaInfo(ctx context.Context, objAPI bms.ExpiryLCConfig = &expLclCfgStr // if all non expiry rules only, ExpiryUpdatedAt would be nil if meta.lifecycleConfig.ExpiryUpdatedAt != nil { - bms.ExpiryLCConfigUpdatedAt = *(meta.lifecycleConfig.ExpiryUpdatedAt) + bms.ExpiryLCConfigUpdatedAt = *meta.lifecycleConfig.ExpiryUpdatedAt } } @@ -4459,6 +4858,7 @@ func (c *SiteReplicationSys) healBuckets(ctx context.Context, objAPI ObjectLayer c.healVersioningMetadata(ctx, objAPI, bucket, info) c.healOLockConfigMetadata(ctx, objAPI, bucket, info) c.healSSEMetadata(ctx, objAPI, bucket, info) + c.healCORSMetadata(ctx, objAPI, bucket, info) c.healBucketReplicationConfig(ctx, objAPI, bucket, info, &opts) c.healBucketPolicies(ctx, objAPI, bucket, info) c.healTagMetadata(ctx, objAPI, bucket, info) @@ -4916,6 +5316,70 @@ func (c *SiteReplicationSys) healSSEMetadata(ctx context.Context, objAPI ObjectL return nil } +func latestCORSConfig(bs map[string]srBucketStatsSummary) (latestID string, latest corsReplicationState, ok bool) { + for dID, status := range bs { + state, err := corsReplicationStateFromInfo(status.meta.SRBucketInfo) + if err != nil || state.kind == corsReplicationBaseline { + continue + } + cmp := compareCORSReplicationStates(latest, state) + if !ok || cmp < 0 || (cmp == 0 && dID > latestID) { + latestID = dID + latest = state + ok = true + } + } + return latestID, latest, ok +} + +func (c *SiteReplicationSys) healCORSMetadata(ctx context.Context, objAPI ObjectLayer, bucket string, info srStatusInfo) error { + c.RLock() + defer c.RUnlock() + if !c.enabled { + return nil + } + + bs := info.BucketStats[bucket] + latestID, latestState, ok := latestCORSConfig(bs) + if !ok { + return nil + } + + latestPeerName := info.Sites[latestID].Name + latestCorsConfig := latestState.encodedPayload() + + for dID, bStatus := range bs { + currentState, err := corsReplicationStateFromInfo(bStatus.meta.SRBucketInfo) + if err == nil && equalCORSReplicationStates(latestState, currentState) { + continue + } + if dID == globalDeploymentID() { + if err := c.PeerBucketCorsConfigHandler(ctx, bucket, latestCorsConfig, latestState.updatedAt); err != nil { + replLogIf(ctx, fmt.Errorf("Unable to heal CORS metadata from peer site %s : %w", latestPeerName, err)) + } + continue + } + + admClient, err := c.getAdminClient(ctx, dID) + if err != nil { + return wrapSRErr(err) + } + peerName := info.Sites[dID].Name + err = admClient.SRPeerReplicateBucketMeta(ctx, madmin.SRBucketMeta{ + Type: madmin.SRBucketMetaTypeCorsConfig, + Bucket: bucket, + Cors: latestCorsConfig, + UpdatedAt: latestState.updatedAt, + }) + if err != nil { + replLogIf(ctx, c.annotatePeerErr(peerName, replicateBucketMetadata, + fmt.Errorf("Unable to heal CORS config metadata for peer %s from peer %s : %w", + peerName, latestPeerName, err))) + } + } + return nil +} + func (c *SiteReplicationSys) healOLockConfigMetadata(ctx context.Context, objAPI ObjectLayer, bucket string, info srStatusInfo) error { bs := info.BucketStats[bucket] @@ -4976,12 +5440,7 @@ func (c *SiteReplicationSys) healOLockConfigMetadata(ctx context.Context, objAPI return wrapSRErr(err) } peerName := info.Sites[dID].Name - err = admClient.SRPeerReplicateBucketMeta(ctx, madmin.SRBucketMeta{ - Type: madmin.SRBucketMetaTypeObjectLockConfig, - Bucket: bucket, - Tags: latestObjLockConfig, - UpdatedAt: lastUpdate, - }) + err = admClient.SRPeerReplicateBucketMeta(ctx, newSRBucketObjectLockMeta(bucket, latestObjLockConfig, lastUpdate)) if err != nil { replLogIf(ctx, c.annotatePeerErr(peerName, replicateBucketMetadata, fmt.Errorf("Unable to heal object lock config metadata for peer %s from peer %s : %w", @@ -5232,7 +5691,7 @@ func isBucketMetadataEqual(one, two *string) bool { case one == nil || two == nil: return false default: - return strings.EqualFold(*one, *two) + return *one == *two } } diff --git a/cmd/site-replication_test.go b/cmd/site-replication_test.go index 397bb9f99..6f4064b04 100644 --- a/cmd/site-replication_test.go +++ b/cmd/site-replication_test.go @@ -18,7 +18,10 @@ package cmd import ( + "encoding/base64" + "encoding/json" "testing" + "time" "github.com/minio/madmin-go/v3" "github.com/minio/minio-go/v7/pkg/set" @@ -66,3 +69,92 @@ func TestGetMissingSiteNames(t *testing.T) { } } } + +// TestSRBucketMetaCorsRoundTrip verifies that a CORS bucket-meta item +// survives the JSON transport used by SRPeerReplicateBucketItem and that +// the base64-encoded payload decodes back to the original XML bytes. This +// mirrors the initial-sync push, the peer-apply path, and the heal path, +// all of which carry the config through SRBucketMeta.Cors as base64. +func TestSRBucketMetaCorsRoundTrip(t *testing.T) { + const corsXML = `https://app.example.comGET` + b64 := base64.StdEncoding.EncodeToString([]byte(corsXML)) + updatedAt := time.Now().UTC().Truncate(time.Second) + + item := madmin.SRBucketMeta{ + Type: madmin.SRBucketMetaTypeCorsConfig, + Bucket: "testbucket", + Cors: &b64, + UpdatedAt: updatedAt, + } + + data, err := json.Marshal(item) + if err != nil { + t.Fatalf("marshal failed: %v", err) + } + + var got madmin.SRBucketMeta + if err := json.Unmarshal(data, &got); err != nil { + t.Fatalf("unmarshal failed: %v", err) + } + + if got.Type != madmin.SRBucketMetaTypeCorsConfig { + t.Fatalf("type mismatch: got %q", got.Type) + } + if got.Cors == nil { + t.Fatal("expected non-nil Cors after round-trip") + } + decoded, err := base64.StdEncoding.DecodeString(*got.Cors) + if err != nil { + t.Fatalf("decode failed: %v", err) + } + if string(decoded) != corsXML { + t.Fatalf("payload mismatch:\n got %q\nwant %q", decoded, corsXML) + } + if !got.UpdatedAt.Equal(updatedAt) { + t.Fatalf("UpdatedAt mismatch: got %v want %v", got.UpdatedAt, updatedAt) + } + + // A deletion is signaled with a nil Cors pointer; it must survive too. + del := madmin.SRBucketMeta{ + Type: madmin.SRBucketMetaTypeCorsConfig, + Bucket: "testbucket", + Cors: nil, + UpdatedAt: updatedAt, + } + data, err = json.Marshal(del) + if err != nil { + t.Fatalf("marshal (delete) failed: %v", err) + } + var gotDel madmin.SRBucketMeta + if err := json.Unmarshal(data, &gotDel); err != nil { + t.Fatalf("unmarshal (delete) failed: %v", err) + } + if gotDel.Cors != nil { + t.Fatalf("expected nil Cors for deletion, got %q", *gotDel.Cors) + } +} + +// TestIsBucketMetadataEqualCors covers the pointer-comparison helper used by +// the CORS heal path to decide whether a peer already holds the latest config. +func TestIsBucketMetadataEqualCors(t *testing.T) { + a := base64.StdEncoding.EncodeToString([]byte("config-a")) + b := base64.StdEncoding.EncodeToString([]byte("config-b")) + + cases := []struct { + name string + one *string + two *string + want bool + }{ + {"both nil", nil, nil, true}, + {"one nil", &a, nil, false}, + {"other nil", nil, &b, false}, + {"equal", &a, &a, true}, + {"different", &a, &b, false}, + } + for _, tc := range cases { + if got := isBucketMetadataEqual(tc.one, tc.two); got != tc.want { + t.Errorf("%s: got %v want %v", tc.name, got, tc.want) + } + } +} diff --git a/cmd/storage-rest-client.go b/cmd/storage-rest-client.go index b96393ee3..76993bc06 100644 --- a/cmd/storage-rest-client.go +++ b/cmd/storage-rest-client.go @@ -39,7 +39,7 @@ import ( xhttp "github.com/minio/minio/internal/http" xioutil "github.com/minio/minio/internal/ioutil" "github.com/minio/minio/internal/rest" - xnet "github.com/minio/pkg/v3/net" + xnet "github.com/pgsty/silo-pkg/v3/net" xbufio "github.com/philhofer/fwd" "github.com/tinylib/msgp/msgp" ) diff --git a/cmd/storage-rest-server.go b/cmd/storage-rest-server.go index 803c24a4e..f20e4e99f 100644 --- a/cmd/storage-rest-server.go +++ b/cmd/storage-rest-server.go @@ -44,7 +44,7 @@ import ( xjwt "github.com/minio/minio/internal/jwt" "github.com/minio/minio/internal/logger" "github.com/minio/mux" - xnet "github.com/minio/pkg/v3/net" + xnet "github.com/pgsty/silo-pkg/v3/net" ) var errDiskStale = errors.New("drive stale") diff --git a/cmd/storage-rest_test.go b/cmd/storage-rest_test.go index a601d7996..d38ad819b 100644 --- a/cmd/storage-rest_test.go +++ b/cmd/storage-rest_test.go @@ -27,7 +27,7 @@ import ( "time" "github.com/minio/minio/internal/grid" - xnet "github.com/minio/pkg/v3/net" + xnet "github.com/pgsty/silo-pkg/v3/net" ) // Storage REST server, storageRESTReceiver and StorageRESTClient are diff --git a/cmd/sts-handlers.go b/cmd/sts-handlers.go index 8b8f9388c..421d4d71b 100644 --- a/cmd/sts-handlers.go +++ b/cmd/sts-handlers.go @@ -41,8 +41,8 @@ import ( xhttp "github.com/minio/minio/internal/http" "github.com/minio/minio/internal/logger" "github.com/minio/mux" - "github.com/minio/pkg/v3/policy" - "github.com/minio/pkg/v3/wildcard" + "github.com/pgsty/silo-pkg/v3/policy" + "github.com/pgsty/silo-pkg/v3/wildcard" ) const ( diff --git a/cmd/sts-handlers_test.go b/cmd/sts-handlers_test.go index 24882745a..214188a14 100644 --- a/cmd/sts-handlers_test.go +++ b/cmd/sts-handlers_test.go @@ -43,7 +43,7 @@ import ( "github.com/minio/minio-go/v7/pkg/set" "github.com/minio/minio-go/v7/pkg/tags" xhttp "github.com/minio/minio/internal/http" - "github.com/minio/pkg/v3/ldap" + "github.com/pgsty/silo-pkg/v3/ldap" ) func runAllIAMSTSTests(suite *TestSuiteIAM, c *check) { diff --git a/cmd/test-utils_test.go b/cmd/test-utils_test.go index 0f903625c..6bd362ffd 100644 --- a/cmd/test-utils_test.go +++ b/cmd/test-utils_test.go @@ -66,7 +66,7 @@ import ( "github.com/minio/minio/internal/hash" "github.com/minio/minio/internal/logger" "github.com/minio/mux" - "github.com/minio/pkg/v3/policy" + "github.com/pgsty/silo-pkg/v3/policy" ) // TestMain to set up global env. @@ -1373,6 +1373,11 @@ func getBucketLifecycleURL(endPoint, bucketName string) (ret string) { return makeTestTargetURL(endPoint, bucketName, "", queryValue) } +// return URL for set/get/delete cors of the bucket. +func getBucketCorsURL(endPoint, bucketName string) string { + return makeTestTargetURL(endPoint, bucketName, "", url.Values{"cors": []string{""}}) +} + // return URL for listing objects in the bucket with V1 legacy API. func getListObjectsV1URL(endPoint, bucketName, prefix, maxKeys, encodingType string) string { queryValue := url.Values{} @@ -2052,6 +2057,15 @@ func registerBucketLevelFunc(bucket *mux.Router, api objectAPIHandlers, apiFunct case "ListenNotification": // Register ListenNotification Handler. bucket.Methods(http.MethodGet).HandlerFunc(api.ListenNotificationHandler).Queries("events", "{events:.*}") + case "PutBucketCors": + // Register PutBucketCors handler. + bucket.Methods(http.MethodPut).HandlerFunc(api.PutBucketCorsHandler).Queries("cors", "") + case "GetBucketCors": + // Register GetBucketCors handler. + bucket.Methods(http.MethodGet).HandlerFunc(api.GetBucketCorsHandler).Queries("cors", "") + case "DeleteBucketCors": + // Register DeleteBucketCors handler. + bucket.Methods(http.MethodDelete).HandlerFunc(api.DeleteBucketCorsHandler).Queries("cors", "") } } } diff --git a/cmd/tier-handlers.go b/cmd/tier-handlers.go index 8c8148749..06fb6c2c3 100644 --- a/cmd/tier-handlers.go +++ b/cmd/tier-handlers.go @@ -27,7 +27,7 @@ import ( "github.com/minio/madmin-go/v3" "github.com/minio/minio/internal/config/storageclass" "github.com/minio/mux" - "github.com/minio/pkg/v3/policy" + "github.com/pgsty/silo-pkg/v3/policy" ) var ( diff --git a/cmd/update.go b/cmd/update.go index e35d8dd1d..315f2e707 100644 --- a/cmd/update.go +++ b/cmd/update.go @@ -39,9 +39,9 @@ import ( "github.com/klauspost/compress/zstd" xhttp "github.com/minio/minio/internal/http" "github.com/minio/minio/internal/logger" - "github.com/minio/pkg/v3/env" - xnet "github.com/minio/pkg/v3/net" "github.com/minio/selfupdate" + "github.com/pgsty/silo-pkg/v3/env" + xnet "github.com/pgsty/silo-pkg/v3/net" gopsutilcpu "github.com/shirou/gopsutil/v3/cpu" ) diff --git a/cmd/utils.go b/cmd/utils.go index 21fe5c61a..7f7032c5f 100644 --- a/cmd/utils.go +++ b/cmd/utils.go @@ -62,9 +62,9 @@ import ( "github.com/minio/minio/internal/logger/message/audit" "github.com/minio/minio/internal/rest" "github.com/minio/mux" - "github.com/minio/pkg/v3/certs" - "github.com/minio/pkg/v3/env" - xnet "github.com/minio/pkg/v3/net" + "github.com/pgsty/silo-pkg/v3/certs" + "github.com/pgsty/silo-pkg/v3/env" + xnet "github.com/pgsty/silo-pkg/v3/net" "golang.org/x/oauth2" ) diff --git a/cmd/veeam-sos-api.go b/cmd/veeam-sos-api.go index 03523e8e1..f82397fe9 100644 --- a/cmd/veeam-sos-api.go +++ b/cmd/veeam-sos-api.go @@ -25,7 +25,6 @@ import ( "os" "strings" - "github.com/minio/madmin-go/v3" "github.com/minio/minio/internal/logger" ) @@ -177,14 +176,7 @@ func veeamSOSAPIGetObject(ctx context.Context, bucket, object string, rs *HTTPRa Used: int64(binfo.Size), } - var quotaSize int64 - if q != nil && q.Type == madmin.HardQuota { - if q.Size > 0 { - quotaSize = int64(q.Size) - } else if q.Quota > 0 { - quotaSize = int64(q.Quota) - } - } + quotaSize := int64(getBucketQuotaSize(q)) if quotaSize == 0 { info := objAPI.StorageInfo(ctx, true) diff --git a/cmd/xl-storage-format-v2-legacy.go b/cmd/xl-storage-format-v2-legacy.go index ec2132279..9d682487b 100644 --- a/cmd/xl-storage-format-v2-legacy.go +++ b/cmd/xl-storage-format-v2-legacy.go @@ -50,7 +50,7 @@ func (x *xlMetaV2VersionHeader) unmarshalV1(bts []byte) (o []byte, err error) { err = msgp.ArrayError{Wanted: 4, Got: zb0001} return o, err } - bts, err = msgp.ReadExactBytes(bts, (x.VersionID)[:]) + bts, err = msgp.ReadExactBytes(bts, x.VersionID[:]) if err != nil { err = msgp.WrapError(err, "VersionID") return o, err @@ -145,7 +145,7 @@ func (z *xlMetaV2VersionHeaderV2) UnmarshalMsg(bts []byte) (o []byte, err error) err = msgp.ArrayError{Wanted: 5, Got: zb0001} return o, err } - bts, err = msgp.ReadExactBytes(bts, (z.VersionID)[:]) + bts, err = msgp.ReadExactBytes(bts, z.VersionID[:]) if err != nil { err = msgp.WrapError(err, "VersionID") return o, err @@ -155,7 +155,7 @@ func (z *xlMetaV2VersionHeaderV2) UnmarshalMsg(bts []byte) (o []byte, err error) err = msgp.WrapError(err, "ModTime") return o, err } - bts, err = msgp.ReadExactBytes(bts, (z.Signature)[:]) + bts, err = msgp.ReadExactBytes(bts, z.Signature[:]) if err != nil { err = msgp.WrapError(err, "Signature") return o, err @@ -195,7 +195,7 @@ func (z *xlMetaV2VersionHeaderV2) DecodeMsg(dc *msgp.Reader) (err error) { err = msgp.ArrayError{Wanted: 5, Got: zb0001} return err } - err = dc.ReadExactBytes((z.VersionID)[:]) + err = dc.ReadExactBytes(z.VersionID[:]) if err != nil { err = msgp.WrapError(err, "VersionID") return err @@ -205,7 +205,7 @@ func (z *xlMetaV2VersionHeaderV2) DecodeMsg(dc *msgp.Reader) (err error) { err = msgp.WrapError(err, "ModTime") return err } - err = dc.ReadExactBytes((z.Signature)[:]) + err = dc.ReadExactBytes(z.Signature[:]) if err != nil { err = msgp.WrapError(err, "Signature") return err diff --git a/cmd/xl-storage-format-v2.go b/cmd/xl-storage-format-v2.go index f3615e187..f50267e25 100644 --- a/cmd/xl-storage-format-v2.go +++ b/cmd/xl-storage-format-v2.go @@ -1101,7 +1101,7 @@ func (x *xlMetaV2) loadLegacy(buf []byte) error { return msgp.WrapError(err, "Versions") } if cap(x.versions) >= int(zb0002) { - x.versions = (x.versions)[:zb0002] + x.versions = x.versions[:zb0002] } else { x.versions = make([]xlMetaV2ShallowVersion, zb0002, zb0002+1) } diff --git a/docs/bucket/notifications/README.md b/docs/bucket/notifications/README.md index 3ba5f1009..5df1ce8fc 100644 --- a/docs/bucket/notifications/README.md +++ b/docs/bucket/notifications/README.md @@ -840,12 +840,11 @@ Received a message: {"EventType":"s3:ObjectCreated:Put","Key":"images/myphoto.jp > database (string) database name (used only if `connection_string` is empty) > ``` > -> These are now deprecated, if you plan to upgrade to any releases after _RELEASE.2020-04-10T03-34-42Z_ make sure -> to migrate to only using _connection_string_ option. To migrate, once you have upgraded all the servers use the -> following command to update the existing notification targets. +> These are now deprecated. SILO does not migrate an enabled target that only has these fields, so convert it to +> _connection_string_ before starting SILO. On the old server, use the following command to update the target. > > ``` -> mc admin config set mysilo/ notify_postgres[:name] connection_string="host=hostname port=2832 username=psqluser password=psqlpass database=bucketevents" +> mc admin config set mysilo/ notify_postgres[:name] connection_string="host=hostname port=2832 user=psqluser password=psqlpass dbname=bucketevents" > ``` > > Please make sure this step is carried out, without this step PostgreSQL notification targets will not work, @@ -973,9 +972,8 @@ key | value > database (string) database name (used only if `dsn_string` is empty) > ``` > -> These are now deprecated, if you plan to upgrade to any releases after _RELEASE.2020-04-10T03-34-42Z_ make sure -> to migrate to only using _dsn_string_ option. To migrate, once you have upgraded all the servers use the -> following command to update the existing notification targets. +> These are now deprecated. SILO does not migrate an enabled target that only has these fields, so convert it to +> _dsn_string_ before starting SILO. On the old server, use the following command to update the target. > > ``` > mc admin config set mysilo/ notify_mysql[:name] dsn_string="mysqluser:mysqlpass@tcp(localhost:2832)/bucketevents" @@ -1045,7 +1043,7 @@ Before updating the configuration, let's start with `mc admin config get` comman ```sh $ mc admin config get mysilo/ notify_mysql -notify_mysql:myinstance enable=off format=namespace host= port= username= password= database= dsn_string= table= queue_dir= queue_limit=0 +notify_mysql:myinstance enable=off format=namespace dsn_string= table= queue_dir= queue_limit=0 ``` Use `mc admin config set` command to update MySQL notification configuration for the deployment with `dsn_string` parameter: diff --git a/docs/bucket/replication/README.md b/docs/bucket/replication/README.md index 9fa0d23fc..5f4d60cb8 100644 --- a/docs/bucket/replication/README.md +++ b/docs/bucket/replication/README.md @@ -96,6 +96,12 @@ The access key provided for the replication *target* cluster should have these m Please note that the permissions required by the admin user on the target cluster can be more fine grained to exclude permissions like "s3:ReplicateDelete", "s3:GetBucketObjectLockConfiguration" etc depending on whether delete replication rules are set up or if object locking is disabled on `destbucket`. The above policies assume that replication of objects, tags and delete marker replication are all enabled on object lock enabled buckets. A sample script to setup replication is provided [here](https://github.com/pgsty/silo/blob/main/docs/bucket/replication/setup_replication.sh) +The target replication credential continues to authorize replicated deletes with +`s3:DeleteObject` plus `s3:ReplicateDelete`; it does not need +`s3:DeleteObjectVersion`. This internal receiver contract is deliberately +separate from ordinary S3 requests: a client deleting an explicitly named +version, including `versionId=null`, must have `s3:DeleteObjectVersion`. + To set up replication from `srcbucket` on the `mysilo` cluster to `destbucket` on a target Silo cluster at `https://replica-endpoint:9000`, use: ``` @@ -200,6 +206,10 @@ To add a replication rule allowing both delete marker replication, versioned del Additional permission of "s3:ReplicateDelete" action would need to be specified on the access key configured for the target cluster if Delete Marker replication or versioned delete replication is enabled. +An explicit deny on `s3:DeleteObjectVersion` still blocks the corresponding +replicated version purge. An allow is not otherwise required for the target +replication credential. + ``` mc replicate add mysilo/srcbucket/Tax --priority 1 --remote-bucket `remote-target` --tags "Year=2019&Company=AcmeCorp" --storage-class "STANDARD" --replicate "delete,delete-marker" Replication configuration applied successfully to mysilo/srcbucket. diff --git a/docs/bucket/replication/delete-replication.sh b/docs/bucket/replication/delete-replication.sh index 507ab7f56..f530369ba 100755 --- a/docs/bucket/replication/delete-replication.sh +++ b/docs/bucket/replication/delete-replication.sh @@ -88,11 +88,7 @@ echo "=== mysilo2" versionId="$(./mc ls --json --versions mysilo1/testbucket/dir/ | tail -n1 | jq -r .versionId)" -export AWS_ACCESS_KEY_ID=minioadmin -export AWS_SECRET_ACCESS_KEY=minioadmin -export AWS_REGION=us-east-1 - -aws s3api --endpoint-url http://localhost:9001 delete-object --bucket testbucket --key dir/file --version-id "$versionId" +./mc rm --version-id "$versionId" mysilo1/testbucket/dir/file ./mc ls -r --versions mysilo1/testbucket >/tmp/mysilo1.txt ./mc ls -r --versions mysilo2/testbucket >/tmp/mysilo2.txt @@ -117,6 +113,76 @@ if [ $ret -ne 0 ]; then exit 1 fi +# Verify the documented least-privilege target policy. Explicit version +# deletion on the receiver is replication traffic, so the target credential +# needs DeleteObject + ReplicateDelete but not DeleteObjectVersion. +./mc mb mysilo1/leastpriv/ mysilo2/leastpriv/ --with-versioning +./mc admin user add mysilo2 repluser repluser123 +cat >/tmp/xl/replpolicy.json <<'EOF' +{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": [ + "s3:GetReplicationConfiguration", + "s3:ListBucket", + "s3:ListBucketMultipartUploads", + "s3:GetBucketLocation", + "s3:GetBucketVersioning" + ], + "Resource": ["arn:aws:s3:::leastpriv"] + }, + { + "Effect": "Allow", + "Action": [ + "s3:GetReplicationConfiguration", + "s3:ReplicateTags", + "s3:AbortMultipartUpload", + "s3:GetObject", + "s3:GetObjectVersion", + "s3:GetObjectVersionTagging", + "s3:PutObject", + "s3:DeleteObject", + "s3:ReplicateObject", + "s3:ReplicateDelete" + ], + "Resource": ["arn:aws:s3:::leastpriv/*"] + } + ] +} +EOF +./mc admin policy create mysilo2 replpolicy /tmp/xl/replpolicy.json +./mc admin policy attach mysilo2 replpolicy --user repluser +./mc replicate add mysilo1/leastpriv --remote-bucket http://repluser:repluser123@localhost:9002/leastpriv/ --priority 1 --replicate delete,delete-marker + +./mc cp README.md mysilo1/leastpriv/dir/file +./mc cp README.md mysilo1/leastpriv/dir/file +sleep 1s + +leastPrivVersionId="$(./mc ls --json --versions mysilo1/leastpriv/dir/ | tail -n1 | jq -r .versionId)" +./mc rm --version-id "$leastPrivVersionId" mysilo1/leastpriv/dir/file +sleep 1s +./mc ls -r --versions mysilo1/leastpriv >/tmp/leastpriv1.txt +./mc ls -r --versions mysilo2/leastpriv >/tmp/leastpriv2.txt +out=$(diff -qpruN /tmp/leastpriv1.txt /tmp/leastpriv2.txt) +ret=$? +if [ $ret -ne 0 ]; then + echo "BUG: least-privilege version delete did not replicate: $out" + exit 1 +fi + +./mc rm mysilo1/leastpriv/dir/file +sleep 1s +./mc ls -r --versions mysilo1/leastpriv >/tmp/leastpriv1.txt +./mc ls -r --versions mysilo2/leastpriv >/tmp/leastpriv2.txt +out=$(diff -qpruN /tmp/leastpriv1.txt /tmp/leastpriv2.txt) +ret=$? +if [ $ret -ne 0 ]; then + echo "BUG: least-privilege delete marker did not replicate: $out" + exit 1 +fi + # Test listing of non replicated permanent deletes set -x @@ -129,7 +195,7 @@ versionId="$(./mc ls --json --versions mysilo1/foobucket/dir/ | jq -r .versionId kill ${pid2} && wait ${pid2} || true -aws s3api --endpoint-url http://localhost:9001 delete-object --bucket foobucket --key dir/file --version-id "$versionId" +./mc rm --version-id "$versionId" mysilo1/foobucket/dir/file out="$(./mc ls mysilo1/foobucket/dir/)" if [ "$out" != "" ]; then diff --git a/docs/compression/README.md b/docs/compression/README.md index 2eb208ea4..1c6194031 100644 --- a/docs/compression/README.md +++ b/docs/compression/README.md @@ -86,6 +86,13 @@ To enable compression+encryption use: Or alternatively through the environment variable `MINIO_COMPRESSION_ALLOW_ENCRYPTION=on`. +SSE-C objects are excluded from compression even with `allow_encryption=on`. +Replication ships an SSE-C object as raw ciphertext, because the server never holds the +customer key, and the compression metadata is not carried over the wire. A compressed +SSE-C object would therefore replicate to a replica that decrypts to a compressed stream. +`allow_encryption` still applies to SSE-S3 and SSE-KMS, where the server owns the key and +decompresses before replicating. + ### 4. Excluded Types - Already compressed objects are not fit for compression since they do not have compressible patterns. diff --git a/docs/debugging/inspect/export.go b/docs/debugging/inspect/export.go index 016a412f6..293fc6c5a 100644 --- a/docs/debugging/inspect/export.go +++ b/docs/debugging/inspect/export.go @@ -358,7 +358,7 @@ func (z *xlMetaV2VersionHeaderV2) UnmarshalMsg(bts []byte) (o []byte, e error) { e = msgp.ArrayError{Wanted: 5, Got: zb0001} return o, e } - bts, e = msgp.ReadExactBytes(bts, (z.VersionID)[:]) + bts, e = msgp.ReadExactBytes(bts, z.VersionID[:]) if e != nil { e = msgp.WrapError(e, "VersionID") return o, e @@ -368,7 +368,7 @@ func (z *xlMetaV2VersionHeaderV2) UnmarshalMsg(bts []byte) (o []byte, e error) { e = msgp.WrapError(e, "ModTime") return o, e } - bts, e = msgp.ReadExactBytes(bts, (z.Signature)[:]) + bts, e = msgp.ReadExactBytes(bts, z.Signature[:]) if e != nil { e = msgp.WrapError(e, "Signature") return o, e diff --git a/docs/debugging/reorder-disks/go.mod b/docs/debugging/reorder-disks/go.mod index e22fcd9e4..8395c0a02 100644 --- a/docs/debugging/reorder-disks/go.mod +++ b/docs/debugging/reorder-disks/go.mod @@ -1,7 +1,5 @@ module github.com/minio/minio/docs/debugging/reorder-disks -go 1.21 +go 1.26.0 -toolchain go1.24.8 - -require github.com/minio/pkg/v3 v3.0.1 +require github.com/pgsty/silo-pkg/v3 v3.13.2 diff --git a/docs/debugging/reorder-disks/go.sum b/docs/debugging/reorder-disks/go.sum index 0cdd474b0..cfc1a44b1 100644 --- a/docs/debugging/reorder-disks/go.sum +++ b/docs/debugging/reorder-disks/go.sum @@ -1,2 +1,2 @@ -github.com/minio/pkg/v3 v3.0.1 h1:qts6g9rYjAdeomRdwjnMc1IaQ6KbaJs3dwqBntXziaw= -github.com/minio/pkg/v3 v3.0.1/go.mod h1:53gkSUVHcfYoskOs5YAJ3D99nsd2SKru90rdE9whlXU= +github.com/pgsty/silo-pkg/v3 v3.13.2 h1:Clw11c/J54Tx6pijNCWtXiC7e0fwP/f5Tgeb6fsXg2w= +github.com/pgsty/silo-pkg/v3 v3.13.2/go.mod h1:0GmaDA0ArQ8bkAI/obiSNTBQzdgBu6W0e0olDCbyXXo= diff --git a/docs/debugging/reorder-disks/main.go b/docs/debugging/reorder-disks/main.go index 5581ca833..9e844a128 100644 --- a/docs/debugging/reorder-disks/main.go +++ b/docs/debugging/reorder-disks/main.go @@ -30,7 +30,7 @@ import ( "strings" "syscall" - "github.com/minio/pkg/v3/ellipses" + "github.com/pgsty/silo-pkg/v3/ellipses" ) type xl struct { diff --git a/docs/debugging/xl-meta/main.go b/docs/debugging/xl-meta/main.go index 23c88d580..a30a037ee 100644 --- a/docs/debugging/xl-meta/main.go +++ b/docs/debugging/xl-meta/main.go @@ -745,7 +745,7 @@ func (z *xlMetaV2VersionHeaderV2) UnmarshalMsg(bts []byte, hdrVer uint) (o []byt err = msgp.ArrayError{Wanted: want, Got: zb0001} return o, err } - bts, err = msgp.ReadExactBytes(bts, (z.VersionID)[:]) + bts, err = msgp.ReadExactBytes(bts, z.VersionID[:]) if err != nil { err = msgp.WrapError(err, "VersionID") return o, err @@ -755,7 +755,7 @@ func (z *xlMetaV2VersionHeaderV2) UnmarshalMsg(bts []byte, hdrVer uint) (o []byt err = msgp.WrapError(err, "ModTime") return o, err } - bts, err = msgp.ReadExactBytes(bts, (z.Signature)[:]) + bts, err = msgp.ReadExactBytes(bts, z.Signature[:]) if err != nil { err = msgp.WrapError(err, "Signature") return o, err diff --git a/docs/metrics/healthcheck/README.md b/docs/metrics/healthcheck/README.md index d2f5d185f..b5904a5f8 100644 --- a/docs/metrics/healthcheck/README.md +++ b/docs/metrics/healthcheck/README.md @@ -50,7 +50,7 @@ livenessProbe: ## Readiness probe -This probe always responds with '200 OK'. Only fails if 'etcd' is configured and unreachable. When readiness probe fails, Kubernetes like platforms turn-off routing to the container. +This probe responds with '200 OK' once the server process is up: it fails only while the request queue is overloaded, or when the health operation against the configured KMS or 'etcd' fails. When readiness probe fails, Kubernetes like platforms turn-off routing to the container. ``` readinessProbe: @@ -120,3 +120,9 @@ X-Xss-Protection: 1; mode=block X-Minio-Write-Quorum: 3 Date: Tue, 21 Jul 2020 00:35:43 GMT ``` + +## Startup readiness window + +None of the probes above, and no `admin info` view, proves that the node which received the request can already serve the data path after a restart. Each node connects its erasure drives to its peers in a monitor loop: a remote drive that could not be connected during startup stays uninstalled on that node until a later pass, and the monitor waits 15 seconds after each completed pass, so that interval is a floor between attempts, not a bound on recovery. While a drive is uninstalled, the node's own liveness and readiness probes answer '200 OK', the cluster probes can report healthy as well, because they aggregate every peer's report of its own local drives rather than the drives this node has installed, and `mcli ready` inherits the same blind spot. Yet a PUT through that node can fail with '503 SlowDownWrite' for lack of write quorum, and a GET through it of an object that another node just wrote can answer '404 NoSuchKey'. In the review runs that established this, sampled I/O began succeeding roughly 13 to 15 seconds after the administrative views became healthy on a four-node loopback cluster, consistent with the reconnect interval, and every object acknowledged by other nodes during the window was readable afterwards; these are observations, not guarantees, since reconnection can keep failing. + +Automation that restarts a cluster and then immediately writes to it, such as an upgrade or failover runbook, should therefore gate on a bounded data-path check rather than on these probes: one small PUT through each node followed by a read of each object through every node, repeated until every request returns the correct bytes and the acknowledged version within one fixed deadline, with each request budgeted from the remaining deadline and SDK retries disabled, and with the acknowledged objects re-read afterwards. Record the time to first usable I/O separately from the probe result. Such a check proves sampled I/O at that moment for the erasure sets those keys hash to; it proves neither that every set is complete nor that there is headroom for a further node loss, since a set can admit writes with fewer than all of its drives installed. The probes remain the right signal for their stated purpose, process liveness and quorum membership, and are unchanged. diff --git a/docs/security/advisories.md b/docs/security/advisories.md index 6860ae33e..7ac4b2364 100644 --- a/docs/security/advisories.md +++ b/docs/security/advisories.md @@ -1,9 +1,17 @@ -# 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--` 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. +## Inherited upstream advisory baseline + +The first Silo community release was cut from upstream history that already contained the following security fix. Upstream and Silo links are both recorded even when the fork preserves the same commit object and SHA; that identity is the inheritance evidence, not a claim that Silo independently reimplemented the patch. + +| ID | Upstream remediation | Silo inheritance | Regression evidence | Release / operator note | +| :-- | :-- | :-- | :-- | :-- | +| [CVE-2025-62506](https://github.com/advisories/GHSA-jjjj-jwhf-8rgr) | [minio/minio#21642](https://github.com/minio/minio/pull/21642), merged as [`c1a49490`](https://github.com/minio/minio/commit/c1a49490c78e9c3ebcad86ba0662319138ace190) | The same commit object is present as [`pgsty/silo@c1a49490`](https://github.com/pgsty/silo/commit/c1a49490c78e9c3ebcad86ba0662319138ace190) | The inherited [service-account](https://github.com/pgsty/silo/blob/c1a49490c78e9c3ebcad86ba0662319138ace190/cmd/admin-handlers-users_test.go#L211-L212) and [STS](https://github.com/pgsty/silo/blob/c1a49490c78e9c3ebcad86ba0662319138ace190/cmd/sts-handlers_test.go#L45-L46) regression groups run for root and non-root parents through `go test ./cmd` | Resets `DenyOnly` while evaluating a restricted session policy so service or STS accounts cannot mint an unrestricted child service account. Upstream first fixed this in [`RELEASE.2025-10-15T17-29-55Z`](https://github.com/minio/minio/releases/tag/RELEASE.2025-10-15T17-29-55Z); every Silo community release, beginning with [`RELEASE.2025-12-03T12-00-00Z`](https://github.com/pgsty/silo/releases/tag/RELEASE.2025-12-03T12-00-00Z), contains it. Operators migrating from an older upstream build should upgrade and audit service accounts created by restricted service or STS identities. | + ## Advisories since `RELEASE.2026-03-21T00-00-00Z` | ID | Fixed by | Affected area | Remote exploitability | Summary | Upgrade / workaround notes | @@ -16,9 +24,15 @@ Entries carry a CVE identifier where one exists. Where none does, they carry a f | [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 @@ -27,11 +41,15 @@ Entries carry a CVE identifier where one exists. Where none does, they carry a f | `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.1](https://go.dev/doc/devel/release#go1.27.1) 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 (1.27.1 as of the release) 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-6354](https://pkg.go.dev/vuln/GO-2026-6354) / [GO-2026-6355](https://pkg.go.dev/vuln/GO-2026-6355) | `golang.org/x/crypto` `v0.56.0` ([`edf36bcbf`](https://github.com/pgsty/silo/commit/edf36bcbf)) | Updates `x/crypto/ssh` to the first fixed version for denial of service on deadlocked undecided and established channels. Reachable through the SFTP server (`startSFTPServer` → `sftp.Server.Listen` → `ssh.NewServerConn`); every earlier release that enables SFTP is affected. | | [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. | +| [CVE-2026-84304](https://github.com/advisories/GHSA-vp52-pcj8-j9qc) | gRPC `v1.83.1` | Updates gRPC-Go to the first fixed version for unauthenticated heap exhaustion through highly fragmented HTTP/2 DATA frames. Silo pulls gRPC transitively rather than registering a gRPC server itself, but selects the fixed version for the complete module graph. | | [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. | ## Operationally significant security-related fixes | Change | Fixed by | Summary | | :-- | :-- | :-- | +| Replicated Object Lock updates ignored their timestamps | pre-release cleanup for the release after 20260806 | A replicated `CopyObject` rebuilt the metadata from the request before comparing replication timestamps, so the stored retention and legal-hold timestamps were never seen: any replica update was applied regardless of order, and the legal-hold timestamp was written under the retention key. A stale replica could therefore turn a newer legal hold off or shorten a newer retention. The stored state is now captured first, a replica update is applied only when its timestamp is newer, a stale one leaves the stored state in place, and each timestamp is kept under its own key. Inherited from upstream; every earlier release is affected. | | LDAP TLS regression | `ce1c537eb` | Restores TLS configuration propagation for `ldaps://` `DialURL()` connections so `MINIO_IDENTITY_LDAP_TLS_SKIP_VERIFY` and custom root CAs work again. | diff --git a/docs/site-replication/CORS-LWW-DESIGN.md b/docs/site-replication/CORS-LWW-DESIGN.md new file mode 100644 index 000000000..70d1d5563 --- /dev/null +++ b/docs/site-replication/CORS-LWW-DESIGN.md @@ -0,0 +1,503 @@ +# Per-Bucket CORS Site-Replication Convergence Design + +## Status + +- 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. +It is an implementation design record, not public upgrade or rollback guidance. +Public operator documentation belongs in the separate `silo.pgsty.com` +repository. + +## Scope + +This design covers the current-version CORS path: + +```text +PutBucketCors / DeleteBucketCors + -> persist local CORS state + -> BucketMetaHook + -> madmin SRBucketMeta transport + -> SRPeerReplicateBucketItem dispatch + -> PeerBucketCorsConfigHandler + -> SiteReplicationMetaInfo + -> siteReplicationStatus + -> latestCORSConfig + -> healCORSMetadata +``` + +It also covers retry, duplicate delivery, reordering, equal timestamps, +initial site sync, missed DELETE recovery, cache reload, process restart, and +concurrent CORS mutations on different nodes of one cluster. + +The following are deliberately out of scope: + +- redesigning the replication semantics of policy, tags, SSE, quota, + versioning, or Object Lock; +- changing higher-level lifecycle merge semantics or serializing bucket + deletion against in-flight metadata updates; those follow-ups remain under + [pgsty/silo#102](https://github.com/pgsty/silo/issues/102); +- mixed-version support that permits CORS writes before every site runs a + CORS-aware binary; +- public downgrade, rollback, and global-fallback documentation; +- Console UI for bucket CORS. + +### Adjacent issue-75 changes in the same candidate + +The final issue-75 candidate also contains CORS work outside the LWW register +itself: + +- a strict, namespace-tolerant XML wire parser that rejects trailing roots, + unknown/nested elements, duplicate singleton fields, invalid integer shape, + and non-whitespace character data; +- Unicode code-point ID counting, exact uppercase S3 methods, non-empty header + elements, and int32-compatible MaxAge validation; +- a single-`*` matcher and response-selection changes needed to distinguish a + literal `*` origin from a patterned or explicit `null` match; +- fail-closed metadata-error handling in the HTTP middleware; +- complete allowed-method, explicit MaxAge=0, expose-header, credentials, and + `Vary` preflight behavior; +- checksum mismatch classification as `BadDigest`; and +- parser, signed-handler, browser-response, and protocol adversarial tests. + +Those changes share the same CORS release gate and are present in the reviewed +diff, but they are not part of the replication conflict key or join algorithm. +This document describes them only where they constrain replication validation +or the final verification boundary. + +## Confirmed Failures in the Pre-Fix Candidate + +The pre-fix issue-75 candidate had four independently reproduced convergence +defects: + +1. Heal compared only payloads. If two sites stored identical payload bytes + with different source timestamps, heal skipped the older site. The sites + retained different ordering barriers and could disagree on a later delayed + event. +2. `isBucketMetadataEqual` used `strings.EqualFold` for base64. For example, + `QQ==` and `qQ==` decode to different bytes but compared equal. +3. Status derived `CorsCfgMismatch` from live payload count and payload set. + It did not include source timestamp or tombstone state, so it could report + divergent sites as converged and suppress healing. +4. Equal-timestamp conflicting events had no stable tie-breaker. Peer apply + accepted whichever event arrived last, while heal selected whichever map + entry happened to be visited first. + +Two additional correctness requirements followed from the state model: + +- the read, compare, and save transition must be atomic across nodes in one + cluster; and +- a successful local PUT or DELETE must advance beyond an already stored + future source timestamp instead of moving the local barrier backwards. + +## Constraints + +The minimum fix must satisfy these constraints: + +- preserve `madmin.SRBucketMeta` and `madmin.SRBucketInfo` wire schemas; +- preserve the exact source `UpdatedAt` on peer apply and heal; +- distinguish a never-configured bucket from a persisted deletion; +- converge without relying on event arrival order, map iteration order, or a + particular site being the healer; +- serialize CORS-versus-CORS transitions cluster-wide without introducing a + broad bucket-metadata redesign; +- reject malformed replication payloads before persistence; +- remain idempotent under retry and initial-sync replay; +- keep the replication state-machine change limited to CORS except for the + directly shared base64 equality bug; do not infer that the same dirty + candidate contains no adjacent CORS protocol or middleware changes. + +## State Model + +For one bucket lineage, the persisted CORS state is: + +```text +State = (Payload, SourceUpdatedAt) +``` + +`BucketMetadata.Created` is not part of the conflict key. It is the lineage +floor used to reject an event from an older incarnation of the bucket. + +### State kinds + +| Kind | Payload | `CorsConfigUpdatedAt` | Meaning | +| --- | --- | --- | --- | +| Baseline | nil | zero | CORS has never been configured for this bucket lineage | +| Live | non-empty XML bytes | non-zero | A live per-bucket CORS configuration | +| Tombstone | nil | non-zero | CORS was explicitly deleted at the source timestamp | + +The baseline uses a zero timestamp deliberately. Defaulting a missing CORS +timestamp to `CreatedAt` would make classification depend on two values that +can be obtained from different cache/disk snapshots. It would also make a +never-configured state indistinguishable from a deletion at bucket creation. + +Per-bucket CORS and `CorsConfigUpdatedAt` were introduced together, so there +is no released legacy live-CORS state that requires synthesizing a timestamp. + +### Wire canonicalization + +A non-nil wire payload must satisfy all of the following: + +1. strict standard base64 decoding succeeds; +2. re-encoding the decoded bytes produces exactly the received string; +3. the decoded payload is non-empty; +4. CORS XML parsing succeeds; and +5. `cors.Config.Validate()` succeeds. + +The canonical re-encode check rejects ignored newlines and alternate textual +representations. Equality is therefore equality of decoded bytes, with exact +base64 string equality remaining safe for the shared metadata helper. + +An invalid wire value is not a candidate winner and is never propagated. +Peer apply rejects it before any metadata write. + +A bucket may nevertheless contain a CORS document written by a pre-release, +more lenient build. Loading such metadata keeps policy, lifecycle, versioning, +and the other bucket fields available, but stashes the CORS parse/validation +error and exposes no active CORS config. CORS GET and middleware lookup return +that error, so browser handling fails closed. A valid PUT or DELETE can repair +the record; any attempt to save a newly invalid CORS document remains rejected. + +## Deterministic Ordering + +States use the following total order: + +```text +1. SourceUpdatedAt +2. Kind: baseline < live < tombstone +3. For live/live ties: lexicographic decoded payload bytes +``` + +The greater state wins. + +Consequences: + +- a newer source event wins regardless of arrival order; +- the same payload with a newer timestamp is a greater state and advances the + ordering barrier; +- a DELETE wins an equal-timestamp PUT/DELETE conflict; +- two equal-timestamp live payloads choose the same bytewise winner at every + site; +- an exact duplicate is equal and therefore a no-op; +- retry, reordering, and duplicate delivery cannot move local state backward. + +The live-payload tie-breaker is not intended to identify the human's temporal +intent. It supplies the deterministic result required when the timestamp has +already failed to distinguish two writes. + +## Why No Source-Site Tie-Breaker + +The rejected source-site alternative ordered states by timestamp plus origin +deployment ID. It would require a new origin field in madmin-go transport and +a persisted origin field in `BucketMetadata`. That adds a dependency release, +wire compatibility work, and an on-disk schema change without improving the +convergence guarantee over the content-based total order. + +If a future product requirement needs provenance-aware conflict explanation, +the source-site design can be introduced as a versioned protocol. It is not +required to make the current register converge. + +## Bucket Lineage and `CreatedAt` + +`CreatedAt` protects a recreated bucket from delayed metadata events belonging +to the prior bucket incarnation: + +```text +if incoming.SourceUpdatedAt < local.CreatedAt: + ignore and log once per bucket +``` + +The floor is retained because removing it could install an old CORS grant on a +new bucket with the same name. It is intentionally not used to classify the +baseline. + +For current-version site replication, local events are generated strictly +after `max(CreatedAt, current CORS barrier)`, and bucket creation timestamps are +propagated before initial metadata sync. A floor rejection therefore indicates +a stale lineage event, clock/history corruption, or a mixed/unsupported setup. +The rejection is observable through a bucket-scoped log-once message and the +remaining status mismatch. + +## Local Transition + +PUT and DELETE use the same CORS-specific transition helper. + +Under the bucket CORS namespace lock: + +1. load the current `.metadata.bin` through the migration-aware parsed loader; +2. validate the new live payload, if any; +3. choose: + + ```text + UpdatedAt = max(UTCNow, CreatedAt + epsilon, CurrentBarrier + epsilon) + ``` + +4. store either the live bytes or a nil tombstone with that timestamp; +5. save and refresh the parsed cache; and +6. release the lock before invoking `BucketMetaHook`. + +This preserves HTTP semantics while ensuring a local administrative action is +strictly greater than the state it observed, including a future-dated peer +barrier caused by clock skew. The peer path deliberately uses a raw metadata +read instead: it must preserve the exact zero baseline and reject missing +metadata rather than implicitly creating a peer bucket record. + +## Peer and Legacy-Bulk Transition + +Typed CORS dispatch decodes and validates the payload, then performs this join +under the same lock: + +```text +if incoming timestamp is zero: + reject +if incoming timestamp is before CreatedAt: + ignore and log +if incoming state <= local state: + no-op +otherwise: + persist incoming payload and exact source timestamp +``` + +The admin handler's legacy/default bulk metadata path can also carry a non-nil +CORS field. It therefore takes the shared metadata lock, applies strict +decoding and validation, and uses the same state comparison before saving. A +nil CORS field in that untyped legacy shape means "not included" and cannot +represent a tombstone; current producers use the typed CORS event for deletion. + +## Concurrency and Locking + +The transition lock is: + +```text +.minio.sys / buckets//metadata.lock +``` + +It is a virtual distributed namespace lock. The name deliberately differs from +the real `buckets//.metadata.bin` object because the metadata save path +locks that object internally and namespace locks are not re-entrant. + +The lock serializes CORS transitions with ordinary `Update`/`Delete`, legacy +bulk metadata, imports, bucket creation/adoption, and metadata migrations. +Every whole-record writer reads the latest disk state while holding the same +lock, so a writer for another configuration type cannot restore stale CORS +columns. Per-type validation, timestamp, and deletion semantics remain +independent. + +No cross-site admin call or `BucketMetaHook` dispatch is made while holding the +metadata lock. The local disk save and resident-cache update complete under the +lock; intra-cluster metadata reload fan-out happens only after release. This +avoids a peer reload that needs migration from waiting on a lock held by the +notifying node. Reordered cross-site delivery is handled by the total-order +join. + +The lock name changes from `cors-config.lock` to `metadata.lock`. During a +rolling upgrade, old and new nodes therefore do not serialize metadata writers +with each other; the shared-lock guarantee begins only after every node in the +cluster runs the new binary. Operators should avoid bucket-metadata changes +during that window. The on-disk record is unchanged, so rollback remains +format-compatible. + +## Dispatch and Retry + +PUT sends a typed `SRBucketMetaTypeCorsConfig` event with canonical base64 XML +and the local source timestamp. DELETE sends the same type with `Cors == nil` +and the tombstone timestamp. + +`BucketMetaHook` may deliver concurrently to sites, fail on a subset, or be +retried by an external operation. The receiver transition is idempotent, so the +transport does not need to impose a global event order. + +Current-version admin dispatch routes the typed event directly to +`PeerBucketCorsConfigHandler`. The legacy/default path is hardened only to +prevent a non-nil CORS field from bypassing the join; it is not a tombstone +compatibility protocol. + +## Status Projection + +`SiteReplicationMetaInfo` always exports `CorsConfigUpdatedAt`, including zero +baseline and nil tombstone states. It exports `CorsConfig` only for a live +payload. + +Status considers sites converged if and only if every site has the same full +CORS state: + +```text +(kind, decoded payload bytes, SourceUpdatedAt) +``` + +Live payload counts remain useful for per-site summary totals, but they do not +determine `CorsCfgMismatch`. + +Examples: + +| Site A | Site B | Mismatch | +| --- | --- | --- | +| baseline | baseline | no | +| same live bytes at same timestamp | same live bytes at same timestamp | no | +| same live bytes at different timestamps | same live bytes at different timestamps | yes | +| same tombstone timestamp | same tombstone timestamp | no | +| tombstones at different timestamps | tombstones at different timestamps | yes | +| live | tombstone | yes | +| invalid wire state | any state | yes | + +## Winner Selection and Heal + +Heal computes the maximum non-baseline valid state using the total order. +Selection is independent of Go map iteration. Deployment ID is used only as a +stable log-source choice when two sites already expose exactly equal states. + +For each different site: + +- the local site delegates to the normal peer CORS transition, preserving the + source timestamp and lock discipline; +- a remote site receives a typed `SRBucketMetaTypeCorsConfig` event with the + winner's canonical payload or nil tombstone and exact timestamp. + +Payload equality alone is insufficient. A site with identical bytes at an +older timestamp is healed so it acquires the same future ordering barrier. + +If every reported state is baseline, there is no event to propagate. If every +reported state is invalid, status remains mismatched and heal does not select +corrupt input as a source. + +## Initial Sync + +Initial sync emits: + +- a live event when `CorsConfigUpdatedAt` is non-zero and payload is live; +- a tombstone event when `CorsConfigUpdatedAt` is non-zero and payload is nil; +- no event for the zero baseline. + +Replaying initial sync is idempotent. A missed DELETE is recoverable because +the tombstone is part of the snapshot rather than being inferred from the +absence of a live payload. + +All sites must run the CORS-aware implementation before enabling or mutating +per-bucket CORS. An older receiver can route an unknown typed event through a +legacy path that cannot represent deletion and does not provide this ordering +contract. + +## Persistence and Restart + +`CorsConfigXML` and `CorsConfigUpdatedAt` are persisted together in +`BucketMetadata` msgpack. Zero time round-trips as zero; CORS is deliberately +not defaulted to `CreatedAt` during load. + +`BucketMetadata.Save` parses the live CORS XML before writing and before the +metadata system replaces the local cache. Therefore a rejected payload cannot +poison disk or cache, and a successful peer/heal transition immediately serves +the newly persisted parsed configuration. + +After cache removal or process restart: + +- a live state restores the same parsed rules and source timestamp; +- a tombstone restores nil payload plus its non-zero timestamp; +- a baseline remains nil plus zero timestamp. +- a legacy-invalid raw document leaves the non-CORS bucket metadata readable, + disables per-bucket CORS fail-closed, and remains repairable through a valid + CORS PUT or DELETE. + +## Error Handling + +| Error | Behavior | +| --- | --- | +| zero source timestamp on a peer live/delete event | reject the event | +| invalid/non-canonical base64 | reject before locking or saving | +| empty non-nil payload | reject | +| malformed XML | reject before saving | +| semantically invalid CORS rules | reject before saving | +| legacy-invalid CORS already on disk | load other metadata, return a CORS-specific error, and permit CORS replacement or deletion | +| event before bucket `CreatedAt` | ignore and log once per bucket | +| missing bucket metadata | return an error; do not create metadata implicitly | +| exact duplicate or lower state | successful no-op | +| remote heal failure | log the peer error; future heal cycles retry | + +## Alternatives Considered + +### Timestamp only + +Rejected. Ignoring or accepting every equal-timestamp conflict leaves an +already divergent pair without a deterministic repair rule. + +### Timestamp plus source deployment ID + +Rejected for the current protocol. It is convergent, but requires madmin-go, +wire, and persisted-schema changes without improving convergence over the +selected total order. + +### Payload-only status and heal + +Rejected. It cannot distinguish ordering barriers and suppresses the exact +heal needed to make later event acceptance consistent. + +### Default baseline timestamp to bucket creation + +Rejected. It conflates baseline classification with a mutable value that may +come from a different snapshot and can turn a never-configured site into a +false tombstone source. + +### Reuse `.metadata.bin` as the transition lock + +Rejected. The save path takes the same namespace lock internally; reusing it +would self-deadlock. + +### Give every metadata type a new state machine + +Rejected. The shared lock prevents whole-record lost updates without changing +the independent replication, validation, or deletion semantics of policy, +tags, SSE, quota, versioning, and Object Lock. Those semantic audits remain +separate from the persistence fix in issue #102. + +## Invariants + +The implementation is acceptable only while all of these invariants hold: + +1. Zero timestamp plus nil payload is the only baseline representation. +2. Nil payload plus non-zero timestamp is a durable tombstone. +3. A live payload has canonical base64 on the wire, valid CORS XML, and a + non-zero source timestamp. +4. Every intentional current-version CORS state transition and every other + whole-record metadata writer is serialized by `metadata.lock` from the + authoritative disk read through save and local cache publication. +5. Peer apply and heal never replace local state with a lower or equal state. +6. Local PUT and DELETE create a state strictly greater than the state observed + under the lock. +7. A peer event before local bucket creation cannot modify the new bucket + lineage. +8. Status reports convergence only for identical full states. +9. Heal selects the same maximum regardless of arrival order, site, or map + iteration order. +10. Same-payload/newer-timestamp heal advances the older barrier. +11. Initial sync and retry preserve tombstones and source timestamps. +12. Disk reload and cache reload preserve state kind, payload, and timestamp. +13. A legacy-invalid CORS document cannot activate global fallback, hide other + bucket metadata, or prevent a valid CORS PUT/DELETE repair. + +## Test Contract + +The required test matrix is: + +| Area | Required evidence | +| --- | --- | +| Wire | canonical base64 accepted; case-different decoded bytes differ; malformed and non-canonical base64 rejected | +| Validation | invalid XML and semantically invalid origin/method/rule rejected without mutation | +| Strict wire | standard S3 namespace accepted; trailing root, unknown/nested elements, duplicate singleton fields, lowercase methods, byte-counted Unicode IDs, and invalid MaxAge rejected | +| Ordering | older event ignored; newer event applied; duplicate no-op; equal live/live order-independent; equal PUT/DELETE chooses tombstone | +| Barrier | same payload with newer timestamp is persisted and healed | +| Tombstone | delayed PUT cannot resurrect; missed DELETE wins heal; repeated DELETE is idempotent | +| Status | baseline, live, tombstone, payload mismatch, and timestamp-only mismatch classified correctly | +| Winner | three-site equal-timestamp selection remains deterministic across repeated map iteration | +| Concurrency | concurrent peer and legacy-bulk events converge to the total-order maximum | +| Local concurrency | concurrent local PUT/DELETE timestamps are unique and final state matches the last serialized transition | +| Initial sync | baseline omitted; live and tombstone emitted with exact source timestamp | +| Lineage | pre-creation event ignored; post-creation event applied | +| 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 | diff --git a/docs/site-replication/run-ssec-object-replication-with-compression.sh b/docs/site-replication/run-ssec-object-replication-with-compression.sh index 5e9e82aba..9ba2e34de 100755 --- a/docs/site-replication/run-ssec-object-replication-with-compression.sh +++ b/docs/site-replication/run-ssec-object-replication-with-compression.sh @@ -62,13 +62,14 @@ echo "Hello world" >/tmp/data/plainfile echo "Hello from encrypted world" >/tmp/data/encrypted touch /tmp/data/defpartsize shred -s 500M /tmp/data/defpartsize -touch /tmp/data/mpartobj.txt -shred -s 500M /tmp/data/mpartobj.txt +# Compressible, and large enough for a multipart upload, so the object would be +# stored compressed if SSE-C were not excluded from compression. +yes "silo compression and sse-c replication payload" | head -c 100000000 >/tmp/data/mpartobj.txt echo "done" # Enable compression for site silo1 -./mc admin config set silo1 compression enable=on extensions=".txt" --insecure -./mc admin config set silo1 compression allow_encryption=off --insecure +./mc admin config set silo1 compression enable=on extensions=".txt" --insecure || exit_1 +./mc admin config set silo1 compression allow_encryption=on --insecure || exit_1 # Create bucket in source cluster echo "Create bucket in source Silo instance" @@ -80,13 +81,13 @@ echo "Loading objects to source Silo instance" ./mc cp /tmp/data/encrypted silo1/test-bucket/encrypted --enc-c "silo1/test-bucket/encrypted=${TEST_MINIO_ENC_KEY}" --insecure ./mc cp /tmp/data/defpartsize silo1/test-bucket/defpartsize --enc-c "silo1/test-bucket/defpartsize=${TEST_MINIO_ENC_KEY}" --insecure -# Below should fail as compression and SSEC used at the same time -# DISABLED: We must check the response header to see if compression was actually applied -#RESULT=$({ ./mc put /tmp/data/mpartobj.txt silo1/test-bucket/mpartobj.txt --enc-c "silo1/test-bucket/mpartobj.txt=${TEST_MINIO_ENC_KEY}" --insecure; } 2>&1) -#if [[ ${RESULT} != *"Server side encryption specified with SSE-C with compression not allowed"* ]]; then -# echo "BUG: Loading an SSE-C object to site with compression should fail. Succeeded though." -# exit_1 -#fi +# A compressible .txt object written with SSE-C while allow_encryption=on. SSE-C +# is excluded from compression whatever allow_encryption says, because +# replication ships SSE-C objects as raw ciphertext and the wire cannot carry the +# compression metadata. Were the object stored compressed, the replica would hold +# the compressed bytes with no compression marker and decrypt to a raw S2 stream, +# which the size and content checks below detect. +./mc cp /tmp/data/mpartobj.txt silo1/test-bucket/mpartobj.txt --enc-c "silo1/test-bucket/mpartobj.txt=${TEST_MINIO_ENC_KEY}" --insecure # Add replication site ./mc admin replicate add silo1 silo2 --insecure @@ -111,6 +112,11 @@ if [ "${count3}" -ne 1 ]; then echo "BUG: object silo1/test-bucket/defpartsize not found" exit_1 fi +count4=$(./mc ls silo1/test-bucket/mpartobj.txt --insecure | wc -l) +if [ "${count4}" -ne 1 ]; then + echo "BUG: object silo1/test-bucket/mpartobj.txt not found" + exit_1 +fi sleep 120 # List the objects from replicated site @@ -131,6 +137,11 @@ if [ "${repcount3}" -ne 1 ]; then echo "BUG: object test-bucket/defpartsize not replicated" exit_1 fi +repcount4=$(./mc ls silo2/test-bucket/mpartobj.txt --insecure | wc -l) +if [ "${repcount4}" -ne 1 ]; then + echo "BUG: object test-bucket/mpartobj.txt not replicated" + exit_1 +fi # Stat the SSEC objects from source site echo "Stat silo1/test-bucket/encrypted" @@ -145,6 +156,25 @@ stat_out2=$(./mc stat --no-list silo1/test-bucket/defpartsize --enc-c "silo1/tes src_obj2_etag=$(echo "${stat_out2}" | jq '.etag') src_obj2_size=$(echo "${stat_out2}" | jq '.size') src_obj2_md5=$(echo "${stat_out2}" | jq '.metadata."X-Amz-Server-Side-Encryption-Customer-Key-Md5"') +echo "Stat silo1/test-bucket/mpartobj.txt" +./mc stat --no-list silo1/test-bucket/mpartobj.txt --enc-c "silo1/test-bucket/mpartobj.txt=${TEST_MINIO_ENC_KEY}" --insecure --json +# The compression marker reaches the client only as the X-Minio-Compressed +# response header, which the SDK filters out of `mc stat --json`, so read the +# raw HTTP trace instead. The sentinel check keeps the assertion from passing +# vacuously if the trace format ever changes. +stat_trace=$(./mc --debug stat --no-list silo1/test-bucket/mpartobj.txt --enc-c "silo1/test-bucket/mpartobj.txt=${TEST_MINIO_ENC_KEY}" --insecure 2>&1) || exit_1 +if ! grep -qi "X-Amz-Request-Id" <<<"${stat_trace}"; then + echo "BUG: 'mc --debug stat' printed no response headers, so the compression check below proves nothing" + exit_1 +fi +if grep -qi "X-Minio-Compressed" <<<"${stat_trace}"; then + echo "BUG: SSE-C object 'silo1/test-bucket/mpartobj.txt' was stored compressed despite the SSE-C compression exclusion" + exit_1 +fi +stat_out3=$(./mc stat --no-list silo1/test-bucket/mpartobj.txt --enc-c "silo1/test-bucket/mpartobj.txt=${TEST_MINIO_ENC_KEY}" --insecure --json) +src_obj3_etag=$(echo "${stat_out3}" | jq '.etag') +src_obj3_size=$(echo "${stat_out3}" | jq '.size') +src_obj3_md5=$(echo "${stat_out3}" | jq '.metadata."X-Amz-Server-Side-Encryption-Customer-Key-Md5"') # Stat the SSEC objects from replicated site echo "Stat silo2/test-bucket/encrypted" @@ -159,6 +189,12 @@ stat_out2_rep=$(./mc stat --no-list silo2/test-bucket/defpartsize --enc-c "silo2 rep_obj2_etag=$(echo "${stat_out2_rep}" | jq '.etag') rep_obj2_size=$(echo "${stat_out2_rep}" | jq '.size') rep_obj2_md5=$(echo "${stat_out2_rep}" | jq '.metadata."X-Amz-Server-Side-Encryption-Customer-Key-Md5"') +echo "Stat silo2/test-bucket/mpartobj.txt" +./mc stat --no-list silo2/test-bucket/mpartobj.txt --enc-c "silo2/test-bucket/mpartobj.txt=${TEST_MINIO_ENC_KEY}" --insecure --json +stat_out3_rep=$(./mc stat --no-list silo2/test-bucket/mpartobj.txt --enc-c "silo2/test-bucket/mpartobj.txt=${TEST_MINIO_ENC_KEY}" --insecure --json) +rep_obj3_etag=$(echo "${stat_out3_rep}" | jq '.etag') +rep_obj3_size=$(echo "${stat_out3_rep}" | jq '.size') +rep_obj3_md5=$(echo "${stat_out3_rep}" | jq '.metadata."X-Amz-Server-Side-Encryption-Customer-Key-Md5"') # Check the etag and size of replicated SSEC objects if [ "${rep_obj1_etag}" != "${src_obj1_etag}" ]; then @@ -177,10 +213,23 @@ if [ "${rep_obj2_size}" != "${src_obj2_size}" ]; then echo "BUG: Size: '${rep_obj2_size}' of replicated object: 'silo2/test-bucket/defpartsize' doesn't match with source value: '${src_obj2_size}'" exit_1 fi +if [ "${rep_obj3_etag}" != "${src_obj3_etag}" ]; then + echo "BUG: Etag: '${rep_obj3_etag}' of replicated object: 'silo2/test-bucket/mpartobj.txt' doesn't match with source value: '${src_obj3_etag}'" + exit_1 +fi +if [ "${rep_obj3_size}" != "${src_obj3_size}" ]; then + echo "BUG: Size: '${rep_obj3_size}' of replicated object: 'silo2/test-bucket/mpartobj.txt' doesn't match with source value: '${src_obj3_size}'" + exit_1 +fi # Check content of replicated SSEC objects ./mc cat silo2/test-bucket/encrypted --enc-c "silo2/test-bucket/encrypted=${TEST_MINIO_ENC_KEY}" --insecure ./mc cat silo2/test-bucket/defpartsize --enc-c "silo2/test-bucket/defpartsize=${TEST_MINIO_ENC_KEY}" --insecure >/dev/null || exit_1 +./mc cat silo2/test-bucket/mpartobj.txt --enc-c "silo2/test-bucket/mpartobj.txt=${TEST_MINIO_ENC_KEY}" --insecure >/tmp/data/mpartobj.replica || exit_1 +if ! cmp -s /tmp/data/mpartobj.txt /tmp/data/mpartobj.replica; then + echo "BUG: replicated object 'silo2/test-bucket/mpartobj.txt' does not match the source; a compressed SSE-C object decrypts to a raw S2 stream on the replica" + exit_1 +fi # Check the MD5 checksums of encrypted objects from source and target if [ "${src_obj1_md5}" != "${rep_obj1_md5}" ]; then @@ -191,5 +240,9 @@ if [ "${src_obj2_md5}" != "${rep_obj2_md5}" ]; then echo "BUG: MD5 checksum of object 'silo2/test-bucket/defpartsize' doesn't match with source. Expected: '${src_obj2_md5}', Found: '${rep_obj2_md5}'" exit_1 fi +if [ "${src_obj3_md5}" != "${rep_obj3_md5}" ]; then + echo "BUG: MD5 checksum of object 'silo2/test-bucket/mpartobj.txt' doesn't match with source. Expected: '${src_obj3_md5}', Found: '${rep_obj3_md5}'" + exit_1 +fi cleanup diff --git a/go.mod b/go.mod index 40960c9a3..ea9743f5f 100644 --- a/go.mod +++ b/go.mod @@ -1,19 +1,12 @@ module github.com/minio/minio -go 1.26.5 +go 1.27.1 -// Use Pigsty's SILO Console v2.1.1 release while preserving upstream import paths. -// The pseudo-version pins v2.1.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-20260806061103-72fc0a5ea52a +// Console and MC retain their historical module paths for best-effort upstream +// compatibility. Pin the maintained PGSTY implementations used by SILO. +replace github.com/minio/console => github.com/pgsty/silo-console v0.0.0-20260908011343-b39a84ada5e8 -// Use Pigsty's maintained mc fork for Console's embedded client code. -replace github.com/minio/mc => github.com/pgsty/mc v0.0.0-20260801042411-ad10a2a10b76 - -// 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.11.0 follows upstream minio/pkg's 3.11 line and carries the -// minio/minio#20449 bucket-write boundary hardening. -replace github.com/minio/pkg/v3 => github.com/pgsty/silo-pkg/v3 v3.11.0 +replace github.com/minio/mc => github.com/pgsty/mc v0.0.0-20260903063637-a2ef95c035d9 // v22.7.0 does not compile on NetBSD because its unix implementation uses // CLOCK_MONOTONIC, which is unavailable there. Keep the last portable release @@ -29,16 +22,16 @@ tool ( require ( aead.dev/mtls v0.3.0 cloud.google.com/go/storage v1.61.3 - github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.1 - github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 + github.com/Azure/azure-sdk-for-go/sdk/azcore v1.22.0 + github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.14.0 github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.4 github.com/IBM/sarama v1.45.1 github.com/alecthomas/participle v0.7.1 github.com/beevik/ntp v1.5.0 github.com/buger/jsonparser v1.1.2 github.com/cespare/xxhash/v2 v2.3.0 - github.com/cheggaaa/pb v1.0.29 - github.com/coreos/go-oidc/v3 v3.17.0 + github.com/cheggaaa/pb v1.0.30 + github.com/coreos/go-oidc/v3 v3.21.0 github.com/coreos/go-systemd/v22 v22.7.0 github.com/cosnicolaou/pbzip2 v1.0.6 github.com/dchest/siphash v1.2.3 @@ -49,7 +42,7 @@ require ( github.com/felixge/fgprof v0.9.5 github.com/fraugster/parquet-go v0.12.0 github.com/go-ldap/ldap/v3 v3.4.14 - github.com/go-openapi/loads v0.23.3 + github.com/go-openapi/loads v0.25.2 github.com/go-sql-driver/mysql v1.9.3 github.com/gobwas/ws v1.4.0 github.com/golang-jwt/jwt/v4 v4.5.2 @@ -57,15 +50,15 @@ require ( github.com/google/uuid v1.6.0 github.com/inconshreveable/mousetrap v1.1.0 github.com/json-iterator/go v1.1.12 - github.com/klauspost/compress v1.18.7 - github.com/klauspost/cpuid/v2 v2.3.0 + github.com/klauspost/compress v1.20.0 + github.com/klauspost/cpuid/v2 v2.4.0 github.com/klauspost/filepathx v1.1.1 github.com/klauspost/pgzip v1.2.6 github.com/klauspost/readahead v1.4.0 github.com/klauspost/reedsolomon v1.13.3 github.com/lib/pq v1.10.9 github.com/lithammer/shortuuid/v4 v4.2.0 - github.com/miekg/dns v1.1.72 + github.com/miekg/dns v1.1.73 github.com/minio/cli v1.24.2 github.com/minio/console v1.7.6 github.com/minio/csvparser v1.0.0 @@ -75,9 +68,8 @@ 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.0.99 - github.com/minio/mux v1.9.2 - github.com/minio/pkg/v3 v3.6.1 + github.com/minio/minio-go/v7 v7.3.1-0.20260828014306-0e78d3f18efe + github.com/minio/mux v1.10.1 github.com/minio/selfupdate v0.6.0 github.com/minio/simdjson-go v0.4.5 github.com/minio/sio v0.4.3 @@ -89,15 +81,16 @@ require ( github.com/nats-io/stan.go v0.10.4 github.com/ncw/directio v1.0.5 github.com/nsqio/go-nsq v1.1.0 + github.com/pgsty/silo-pkg/v3 v3.13.2 github.com/philhofer/fwd v1.2.0 - github.com/pierrec/lz4/v4 v4.1.26 + github.com/pierrec/lz4/v4 v4.1.29 github.com/pkg/errors v0.9.1 - github.com/pkg/sftp v1.13.10 + github.com/pkg/sftp v1.13.11 github.com/pkg/xattr v0.4.12 - github.com/prometheus/client_golang v1.23.2 - github.com/prometheus/client_model v0.6.2 - github.com/prometheus/common v0.67.5 - github.com/prometheus/procfs v0.20.1 + github.com/prometheus/client_golang v1.24.1 + github.com/prometheus/client_model v0.6.3 + github.com/prometheus/common v0.71.0 + github.com/prometheus/procfs v0.22.0 github.com/puzpuzpuz/xsync/v3 v3.5.1 github.com/rabbitmq/amqp091-go v1.10.0 github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9 @@ -108,26 +101,26 @@ require ( github.com/valyala/bytebufferpool v1.0.0 github.com/xdg/scram v1.0.5 github.com/zeebo/xxh3 v1.1.0 - go.etcd.io/etcd/api/v3 v3.6.9 - go.etcd.io/etcd/client/v3 v3.6.9 + go.etcd.io/etcd/api/v3 v3.7.1 + go.etcd.io/etcd/client/v3 v3.7.1 go.uber.org/atomic v1.11.0 go.uber.org/zap v1.28.0 go.yaml.in/yaml/v3 v3.0.5 - goftp.io/server/v2 v2.0.2 - golang.org/x/crypto v0.54.0 + goftp.io/server/v2 v2.0.3 + golang.org/x/crypto v0.56.0 golang.org/x/oauth2 v0.36.0 golang.org/x/sync v0.22.0 golang.org/x/sys v0.47.0 golang.org/x/term v0.45.0 golang.org/x/time v0.15.0 - google.golang.org/api v0.278.0 + google.golang.org/api v0.290.0 gopkg.in/yaml.v2 v2.4.0 ) require ( aead.dev/mem v0.2.0 // indirect aead.dev/minisign v0.3.0 // indirect - cel.dev/expr v0.25.1 // indirect + cel.dev/expr v0.25.2 // indirect cloud.google.com/go v0.123.0 // indirect cloud.google.com/go/auth v0.20.0 // indirect cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect @@ -137,8 +130,8 @@ require ( filippo.io/edwards25519 v1.2.0 // indirect github.com/Azure/azure-sdk-for-go/sdk/internal v1.12.0 // indirect github.com/Azure/go-ntlmssp v0.1.1 // indirect - github.com/AzureAD/microsoft-authentication-library-for-go v1.7.0 // indirect - github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.32.0 // indirect + github.com/AzureAD/microsoft-authentication-library-for-go v1.7.2 // indirect + github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.33.0 // indirect github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0 // indirect github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.55.0 // indirect github.com/VividCortex/ewma v1.2.0 // indirect @@ -152,7 +145,7 @@ require ( github.com/charmbracelet/colorprofile v0.4.3 // indirect github.com/charmbracelet/harmonica v0.2.0 // indirect github.com/charmbracelet/lipgloss v1.1.0 // indirect - github.com/charmbracelet/x/ansi v0.11.6 // indirect + github.com/charmbracelet/x/ansi v0.11.8 // indirect github.com/charmbracelet/x/cellbuf v0.0.15 // indirect github.com/charmbracelet/x/term v0.2.2 // indirect github.com/clipperhouse/displaywidth v0.11.0 // indirect @@ -169,33 +162,33 @@ require ( github.com/envoyproxy/protoc-gen-validate v1.3.3 // indirect github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect github.com/fatih/structs v1.1.0 // indirect - github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/felixge/httpsnoop v1.1.0 // indirect github.com/go-asn1-ber/asn1-ber v1.5.8 // indirect - github.com/go-ini/ini v1.67.0 // indirect github.com/go-jose/go-jose/v4 v4.1.4 // indirect - github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/logr v1.4.4 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-ole/go-ole v1.3.0 // indirect - github.com/go-openapi/analysis v0.25.0 // indirect - github.com/go-openapi/errors v0.22.7 // indirect - github.com/go-openapi/jsonpointer v0.23.1 // indirect - github.com/go-openapi/jsonreference v0.21.5 // indirect - github.com/go-openapi/runtime v0.29.3 // indirect - github.com/go-openapi/spec v0.22.4 // indirect - github.com/go-openapi/strfmt v0.26.2 // indirect - github.com/go-openapi/swag v0.25.5 // indirect - github.com/go-openapi/swag/cmdutils v0.25.5 // indirect - github.com/go-openapi/swag/conv v0.28.0 // indirect - github.com/go-openapi/swag/fileutils v0.25.5 // indirect - github.com/go-openapi/swag/jsonname v0.26.0 // indirect - github.com/go-openapi/swag/jsonutils v0.25.5 // indirect - github.com/go-openapi/swag/loading v0.25.5 // indirect - github.com/go-openapi/swag/mangling v0.25.5 // indirect - github.com/go-openapi/swag/netutils v0.25.5 // indirect - github.com/go-openapi/swag/stringutils v0.25.5 // indirect - github.com/go-openapi/swag/typeutils v0.28.0 // indirect - github.com/go-openapi/swag/yamlutils v0.25.5 // indirect - github.com/go-openapi/validate v0.25.2 // indirect + github.com/go-openapi/analysis v0.26.2 // indirect + github.com/go-openapi/errors v0.22.8 // indirect + github.com/go-openapi/jsonpointer v1.0.0 // indirect + github.com/go-openapi/jsonreference v1.0.1 // indirect + github.com/go-openapi/runtime v0.33.1 // indirect + github.com/go-openapi/runtime/server-middleware v0.33.1 // indirect + github.com/go-openapi/spec v1.0.0 // indirect + github.com/go-openapi/strfmt v0.27.0 // indirect + github.com/go-openapi/swag v0.29.1 // indirect + github.com/go-openapi/swag/cmdutils v0.29.1 // indirect + github.com/go-openapi/swag/conv v0.29.1 // indirect + github.com/go-openapi/swag/fileutils v0.29.1 // indirect + github.com/go-openapi/swag/jsonutils v0.29.1 // indirect + github.com/go-openapi/swag/loading v0.29.1 // indirect + github.com/go-openapi/swag/mangling v0.29.1 // indirect + github.com/go-openapi/swag/netutils v0.29.1 // indirect + github.com/go-openapi/swag/pools v0.29.1 // indirect + github.com/go-openapi/swag/stringutils v0.29.1 // indirect + github.com/go-openapi/swag/typeutils v0.29.1 // indirect + github.com/go-openapi/swag/yamlutils v0.29.1 // indirect + github.com/go-openapi/validate v0.26.5 // indirect github.com/go-viper/mapstructure/v2 v2.5.0 // indirect github.com/gobwas/httphead v0.1.0 // indirect github.com/gobwas/pool v0.2.1 // indirect @@ -205,51 +198,51 @@ require ( github.com/golang/protobuf v1.5.4 // indirect github.com/golang/snappy v1.0.0 // indirect github.com/google/go-tpm v0.9.8 // indirect - github.com/google/pprof v0.0.0-20260507013755-92041b743c96 // indirect + github.com/google/pprof v0.0.0-20260709232956-b9395ee17fa0 // indirect github.com/google/s2a-go v0.1.9 // indirect github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect - github.com/googleapis/enterprise-certificate-proxy v0.3.15 // indirect - github.com/googleapis/gax-go/v2 v2.22.0 // indirect - github.com/gorilla/mux v1.8.1 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.3.18 // indirect + github.com/googleapis/gax-go/v2 v2.23.0 // indirect github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect github.com/grafana/regexp v0.0.0-20250905093917-f7b3be9d1853 // indirect - github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.30.0 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/hashicorp/go-uuid v1.0.3 // indirect - github.com/hashicorp/golang-lru v1.0.2 // indirect github.com/jcmturner/aescts/v2 v2.0.0 // indirect github.com/jcmturner/dnsutils/v2 v2.0.0 // indirect github.com/jcmturner/gofork v1.7.6 // indirect github.com/jcmturner/gokrb5/v8 v8.4.4 // indirect github.com/jcmturner/rpc/v2 v2.0.3 // indirect - github.com/jedib0t/go-pretty/v6 v6.7.8 // indirect + github.com/jedib0t/go-pretty/v6 v6.8.3 // indirect github.com/jessevdk/go-flags v1.6.1 // indirect github.com/juju/ratelimit v1.0.2 // indirect github.com/klauspost/crc32 v1.3.0 // indirect github.com/kr/fs v0.1.0 // indirect github.com/kylelemons/godebug v1.1.0 // indirect github.com/lestrrat-go/blackmagic v1.0.4 // indirect - github.com/lestrrat-go/dsig v1.0.0 // indirect + github.com/lestrrat-go/dsig v1.4.0 // indirect github.com/lestrrat-go/dsig-secp256k1 v1.0.0 // indirect github.com/lestrrat-go/httpcc v1.0.1 // indirect - github.com/lestrrat-go/httprc/v3 v3.0.1 // indirect - github.com/lestrrat-go/jwx/v3 v3.0.12 // indirect - github.com/lestrrat-go/option v1.0.1 // indirect + github.com/lestrrat-go/httprc/v3 v3.0.6 // indirect + github.com/lestrrat-go/jwx/v3 v3.2.0 // indirect github.com/lestrrat-go/option/v2 v2.0.0 // indirect - github.com/lucasb-eyer/go-colorful v1.3.0 // indirect - github.com/lufia/plan9stats v0.0.0-20260216142805-b3301c5f2a88 // indirect + github.com/lucasb-eyer/go-colorful v1.4.1 // indirect + github.com/lufia/plan9stats v0.0.0-20260802145828-341c2f0c90b5 // indirect github.com/mattn/go-colorable v0.1.15 // indirect github.com/mattn/go-ieproxy v0.0.12 // indirect github.com/mattn/go-isatty v0.0.24 // indirect github.com/mattn/go-localereader v0.0.1 // indirect - github.com/mattn/go-runewidth v0.0.21 // indirect + github.com/mattn/go-runewidth v0.0.29 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect github.com/minio/colorjson v1.0.8 // indirect github.com/minio/crc64nvme v1.1.1 // indirect github.com/minio/filepath v1.0.0 // indirect github.com/minio/mc v0.0.0-20251106162529-77f82e18b540 // indirect github.com/minio/md5-simd v1.1.2 // indirect + // Legacy minio/colorjson and minio/dperf code still imports pkg/console. + // SILO source imports github.com/pgsty/silo-pkg/v3 directly instead. + github.com/minio/pkg/v3 v3.6.1 // indirect github.com/minio/websocket v1.6.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect @@ -262,51 +255,52 @@ require ( github.com/nats-io/nats-streaming-server v0.24.6 // indirect github.com/nats-io/nkeys v0.4.15 // indirect github.com/nats-io/nuid v1.0.1 // indirect - github.com/oklog/ulid/v2 v2.1.1 // indirect + github.com/oklog/ulid/v2 v2.1.2 // indirect github.com/olekukonko/tablewriter v0.0.5 // indirect github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect github.com/posener/complete v1.2.3 // indirect - github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect + github.com/power-devops/perfstat v0.0.0-20260805114148-88456608a4f6 // indirect github.com/prometheus/prom2json v1.5.0 // indirect - github.com/prometheus/prometheus v0.311.3 // indirect + github.com/prometheus/prometheus v0.314.0 // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/rjeczalik/notify v0.9.3 // indirect github.com/rs/xid v1.6.0 // indirect github.com/safchain/ethtool v0.7.0 // indirect github.com/segmentio/asm v1.2.1 // indirect github.com/shoenig/go-m1cpu v0.2.2 // indirect - github.com/spiffe/go-spiffe/v2 v2.6.0 // indirect - github.com/tidwall/gjson v1.18.0 // indirect + github.com/spiffe/go-spiffe/v2 v2.7.0 // indirect + github.com/tidwall/gjson v1.19.0 // indirect github.com/tidwall/match v1.2.0 // indirect github.com/tidwall/pretty v1.2.1 // indirect - github.com/tklauser/go-sysconf v0.3.16 // indirect - github.com/tklauser/numcpus v0.11.0 // indirect + github.com/tklauser/go-sysconf v0.4.0 // indirect + github.com/tklauser/numcpus v0.12.0 // indirect github.com/unrolled/secure v1.17.0 // indirect - github.com/valyala/fastjson v1.6.4 // indirect - github.com/vbauerster/mpb/v8 v8.12.0 // indirect + github.com/valyala/fastjson v1.6.10 // indirect + github.com/vbauerster/cupwriter v0.0.4 // indirect + github.com/vbauerster/mpb/v8 v8.16.1 // indirect github.com/xdg/stringprep v1.0.3 // indirect - github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect + github.com/xo/terminfo v1.0.0 // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect - go.etcd.io/etcd/client/pkg/v3 v3.6.9 // indirect + go.etcd.io/etcd/client/pkg/v3 v3.7.1 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect - go.opentelemetry.io/contrib/detectors/gcp v1.43.0 // indirect - go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0 // indirect - go.opentelemetry.io/otel v1.44.0 // indirect - go.opentelemetry.io/otel/metric v1.44.0 // indirect - go.opentelemetry.io/otel/sdk v1.44.0 // indirect - go.opentelemetry.io/otel/sdk/metric v1.44.0 // indirect - go.opentelemetry.io/otel/trace v1.44.0 // indirect + go.opentelemetry.io/contrib/detectors/gcp v1.44.0 // indirect + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.70.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.70.0 // indirect + go.opentelemetry.io/otel v1.45.0 // indirect + go.opentelemetry.io/otel/metric v1.45.0 // indirect + go.opentelemetry.io/otel/sdk v1.45.0 // indirect + go.opentelemetry.io/otel/sdk/metric v1.45.0 // indirect + go.opentelemetry.io/otel/trace v1.45.0 // indirect go.uber.org/multierr v1.11.0 // indirect - go.yaml.in/yaml/v2 v2.4.4 // indirect - golang.org/x/mod v0.37.0 // indirect - golang.org/x/net v0.57.0 // indirect - golang.org/x/text v0.40.0 // indirect - golang.org/x/tools v0.47.0 // indirect + golang.org/x/mod v0.40.0 // indirect + golang.org/x/net v0.58.0 // indirect + golang.org/x/text v0.41.0 // indirect + golang.org/x/tools v0.49.0 // indirect google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa // indirect - google.golang.org/grpc v1.82.1 // indirect - google.golang.org/protobuf v1.36.11 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260831171406-18b4a7587f8a // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260831171406-18b4a7587f8a // indirect + google.golang.org/grpc v1.83.2 // indirect + google.golang.org/protobuf v1.36.12 // indirect + gopkg.in/ini.v1 v1.67.3 // indirect ) diff --git a/go.sum b/go.sum index 281f052ae..b6dc012d4 100644 --- a/go.sum +++ b/go.sum @@ -5,8 +5,8 @@ aead.dev/minisign v0.3.0 h1:8Xafzy5PEVZqYDNP60yJHARlW1eOQtsKNp/Ph2c0vRA= aead.dev/minisign v0.3.0/go.mod h1:NLvG3Uoq3skkRMDuc3YHpWUTMTrSExqm+Ij73W13F6Y= aead.dev/mtls v0.3.0 h1:a+C0t15Y9SRX6qP1EqmQFZ4ZSMm88TPvNDymasu4ahQ= aead.dev/mtls v0.3.0/go.mod h1:rZvRApIcPkCNu2AgpFoaMxKBee/XVkKs7wEuYgqLI3Q= -cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4= -cel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4= +cel.dev/expr v0.25.2 h1:K6j46C81hXtZQfuX60cVWQFBJahKSE2gfRbNuvr5bFs= +cel.dev/expr v0.25.2/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4= cloud.google.com/go v0.123.0 h1:2NAUJwPR47q+E35uaJeYoNhuNEM9kM8SjgRgdeOJUSE= cloud.google.com/go v0.123.0/go.mod h1:xBoMV08QcqUGuPW65Qfm1o9Y4zKZBpGS+7bImXLTAZU= cloud.google.com/go/auth v0.20.0 h1:kXTssoVb4azsVDoUiF8KvxAqrsQcQtB53DcSgta74CA= @@ -29,12 +29,12 @@ cloud.google.com/go/trace v1.11.7 h1:kDNDX8JkaAG3R2nq1lIdkb7FCSi1rCmsEtKVsty7p+U cloud.google.com/go/trace v1.11.7/go.mod h1:TNn9d5V3fQVf6s4SCveVMIBS2LJUqo73GACmq/Tky0s= filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo= filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc= -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.1 h1:jHb/wfvRikGdxMXYV3QG/SzUOPYN9KEUUuC0Yd0/vC0= -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.1/go.mod h1:pzBXCYn05zvYIrwLgtK8Ap8QcjRg+0i76tMQdWN6wOk= -github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 h1:Hk5QBxZQC1jb2Fwj6mpzme37xbCDdNTxU7O9eb5+LB4= -github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1/go.mod h1:IYus9qsFobWIc2YVwe/WPjcnyCkPKtnHAqUYeebc8z0= -github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2 h1:yz1bePFlP5Vws5+8ez6T3HWXPmwOK7Yvq8QxDBD3SKY= -github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2/go.mod h1:Pa9ZNPuoNu/GztvBSKk9J1cDJW6vk/n0zLtV4mgd8N8= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.22.0 h1:aokoqcHvaGjiM3VpjKDfMMnF/8epJ+Q1HLJ7CudztqE= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.22.0/go.mod h1:/WYEx9pcM9Y+Dd/APJaNlSvVSvzl54rrMdZT5+Oi2LM= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.14.0 h1:CU4+EJeJi3TKYWEcYuSdWsjzw0nVsK/H0MSQOiPcymU= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.14.0/go.mod h1:q0+UTSRvShwUCrR/s5HtyInYphN7Wvxb7snFM3u+SLA= +github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.4.0 h1:xFaZZ+IubdftrDHnGGwZ6QvQ3KHTtWl2MCK+GMt2vxs= +github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.4.0/go.mod h1:mCBhUhlMjLLJKr5aqw2TNS/VqJOie8MzWq3DAMJeKso= github.com/Azure/azure-sdk-for-go/sdk/internal v1.12.0 h1:fhqpLE3UEXi9lPaBRpQ6XuRW0nU7hgg4zlmZZa+a9q4= github.com/Azure/azure-sdk-for-go/sdk/internal v1.12.0/go.mod h1:7dCRMLwisfRH3dBupKeNCioWYUZ4SS09Z14H+7i8ZoY= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.8.1 h1:/Zt+cDPnpC3OVDm/JKLOs7M2DKmLRIIp3XIx9pHHiig= @@ -45,12 +45,12 @@ github.com/Azure/go-ntlmssp v0.1.1 h1:l+FM/EEMb0U9QZE7mKNEDw5Mu3mFiaa2GKOoTSsNDP github.com/Azure/go-ntlmssp v0.1.1/go.mod h1:NYqdhxd/8aAct/s4qSYZEerdPuH1liG2/X9DiVTbhpk= github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJTmL004Abzc5wDB5VtZG2PJk5ndYDgVacGqfirKxjM= github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mod h1:tCcJZ0uHAmvjsVYzEFivsRTN00oz5BEsRgQHu5JZ9WE= -github.com/AzureAD/microsoft-authentication-library-for-go v1.7.0 h1:4iB+IesclUXdP0ICgAabvq2FYLXrJWKx1fJQ+GxSo3Y= -github.com/AzureAD/microsoft-authentication-library-for-go v1.7.0/go.mod h1:HKpQxkWaGLJ+D/5H8QRpyQXA1eKjxkFlOMwck5+33Jk= +github.com/AzureAD/microsoft-authentication-library-for-go v1.7.2 h1:RHK7bS+HQMslb1sZpAokUt+zTVmue0hKSs2C791hhzU= +github.com/AzureAD/microsoft-authentication-library-for-go v1.7.2/go.mod h1:HKpQxkWaGLJ+D/5H8QRpyQXA1eKjxkFlOMwck5+33Jk= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/DataDog/datadog-go v2.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.32.0 h1:rIkQfkCOVKc1OiRCNcSDD8ml5RJlZbH/Xsq7lbpynwc= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.32.0/go.mod h1:RD2SsorTmYhF6HkTmDw7KmPYQk8OBYwTkuasChwv7R4= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.33.0 h1:l7+6kwRMJNwdCvYdDl7Eax+wzEYHSnNY7zrrfbhDdTA= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.33.0/go.mod h1:pJTkW8hEUIIi3Pf65lPZOnn4Y81yCllX6IWk2jNXdkM= github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0 h1:UnDZ/zFfG1JhH/DqxIZYU/1CUAlTUScoXD/LcM2Ykk8= github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0/go.mod h1:IA1C1U7jO/ENqm/vhi7V9YYpBsp+IMyqNrEN94N7tVc= github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.55.0 h1:7t/qx5Ost0s0wbA/VDrByOooURhp+ikYwv20i9Y07TQ= @@ -101,16 +101,16 @@ github.com/charmbracelet/harmonica v0.2.0 h1:8NxJWRWg/bzKqqEaaeFNipOu77YR5t8aSwG github.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao= github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY= github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30= -github.com/charmbracelet/x/ansi v0.11.6 h1:GhV21SiDz/45W9AnV2R61xZMRri5NlLnl6CVF7ihZW8= -github.com/charmbracelet/x/ansi v0.11.6/go.mod h1:2JNYLgQUsyqaiLovhU2Rv/pb8r6ydXKS3NIttu3VGZQ= +github.com/charmbracelet/x/ansi v0.11.8 h1:JMFwp0CgDC2+jcOB162HH5k7I3FVbgFSMMYg7dSPBQQ= +github.com/charmbracelet/x/ansi v0.11.8/go.mod h1:ZNN+3mXny/516oTQPLMPIBeSINvNJJQ8uQXDgbeJxY0= github.com/charmbracelet/x/cellbuf v0.0.15 h1:ur3pZy0o6z/R7EylET877CBxaiE1Sp1GMxoFPAIztPI= github.com/charmbracelet/x/cellbuf v0.0.15/go.mod h1:J1YVbR7MUuEGIFPCaaZ96KDl5NoS0DAWkskup+mOY+Q= github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91 h1:payRxjMjKgx2PaCWLZ4p3ro9y97+TVLZNaRZgJwSVDQ= github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U= github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk= github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI= -github.com/cheggaaa/pb v1.0.29 h1:FckUN5ngEk2LpvuG0fw1GEFx6LtyY2pWI/Z2QgCnEYo= -github.com/cheggaaa/pb v1.0.29/go.mod h1:W40334L7FMC5JKWldsTWbdGjLo0RxUKK73K+TuPxX30= +github.com/cheggaaa/pb v1.0.30 h1:NylhgqJfXx3JVBGx6ywsXuhpz8caSMPmLArXyAv1bwU= +github.com/cheggaaa/pb v1.0.30/go.mod h1:YgTBwa6PqwwDB/2UKdLuuFRNTwEkcCPsA5AmWivrBAg= github.com/chromedp/cdproto v0.0.0-20230802225258-3cf4e6d46a89/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs= github.com/chromedp/chromedp v0.9.2/go.mod h1:LkSXJKONWTCHAfQasKFUZI+mxqS4tZqhmtGzzhLsnLs= github.com/chromedp/sysutil v1.0.0/go.mod h1:kgWmDdq8fTzXYcKIBqIYvRRTnYb9aNS9moAV0xufSww= @@ -127,8 +127,8 @@ github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 h1:aBangftG7EVZoUb69Os github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2/go.mod h1:qwXFYgsP6T7XnJtbKlf1HP8AjxZZyzxMmc+Lq5GjlU4= github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= -github.com/coreos/go-oidc/v3 v3.17.0 h1:hWBGaQfbi0iVviX4ibC7bk8OKT5qNr4klBaCHVNvehc= -github.com/coreos/go-oidc/v3 v3.17.0/go.mod h1:wqPbKFrVnE90vty060SB40FCJ8fTHTxSwyXJqZH+sI8= +github.com/coreos/go-oidc/v3 v3.21.0 h1:wZo4Q9Pum8dYEj0eMUPrqR+kvuGkeUplbLpNCkBqoWM= +github.com/coreos/go-oidc/v3 v3.21.0/go.mod h1:DYCf24+ncYi+XkIH97GY1+dqoRlbaSI26KVTCI9SrY4= github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr4= github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec= @@ -177,8 +177,8 @@ github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= github.com/felixge/fgprof v0.9.5 h1:8+vR6yu2vvSKn08urWyEuxx75NWPEvybbkBirEpsbVY= github.com/felixge/fgprof v0.9.5/go.mod h1:yKl+ERSa++RYOs32d8K6WEXCB4uXdLls4ZaZPpayhMM= -github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= -github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/felixge/httpsnoop v1.1.0 h1:3YtUj32ZZkqZtt3sZZsClsymw/QDuVfpNhoA31zeORc= +github.com/felixge/httpsnoop v1.1.0/go.mod h1:Zqxgdd+1Rkcz8euOqdr7lqgCRJztwr5hp9vDSi5UZCE= github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw= github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= github.com/fraugster/parquet-go v0.12.0 h1:1slnC5y2VWEOUSlzbeXatM0BvSWcLUDsR/EcZsXXCZc= @@ -186,68 +186,68 @@ github.com/fraugster/parquet-go v0.12.0/go.mod h1:dGzUxdNqXsAijatByVgbAWVPlFirnh github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/go-asn1-ber/asn1-ber v1.5.8 h1:H9AZkK22UOmfX8J84ubyaZxKJZ3FMHVwn8swoMML7iQ= github.com/go-asn1-ber/asn1-ber v1.5.8/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= -github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A= -github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA= github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= github.com/go-ldap/ldap/v3 v3.4.14 h1:D6PYdEgsaVzsXyr6w/yDC06Ria4uUhWm+Rb+er8lfAs= github.com/go-ldap/ldap/v3 v3.4.14/go.mod h1:S4eJUMUNjDkE0ZJtIZdybwyb03sGGLW6gxXT1Hs8VKA= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= -github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/logr v1.4.4 h1:tG4xh9yMsRCAiodLVTxyrkzSZ9+o0L1Kg/+cPVcbP/8= +github.com/go-logr/logr v1.4.4/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= -github.com/go-openapi/analysis v0.25.0 h1:EnjAq1yO8wEO9HbPmY8vLPEIkdZuuFhCAKBPvCB7bCs= -github.com/go-openapi/analysis v0.25.0/go.mod h1:5WFTRE43WLkPG9r9OtlMfqkkvUTYLVVCIxLlEpyF8kE= -github.com/go-openapi/errors v0.22.7 h1:JLFBGC0Apwdzw3484MmBqspjPbwa2SHvpDm0u5aGhUA= -github.com/go-openapi/errors v0.22.7/go.mod h1://QW6SD9OsWtH6gHllUCddOXDL0tk0ZGNYHwsw4sW3w= -github.com/go-openapi/jsonpointer v0.23.1 h1:1HBACs7XIwR2RcmItfdSFlALhGbe6S92p0ry4d1GWg4= -github.com/go-openapi/jsonpointer v0.23.1/go.mod h1:iWRmZTrGn7XwYhtPt/fvdSFj1OfNBngqRT2UG3BxSqY= -github.com/go-openapi/jsonreference v0.21.5 h1:6uCGVXU/aNF13AQNggxfysJ+5ZcU4nEAe+pJyVWRdiE= -github.com/go-openapi/jsonreference v0.21.5/go.mod h1:u25Bw85sX4E2jzFodh1FOKMTZLcfifd1Q+iKKOUxExw= -github.com/go-openapi/loads v0.23.3 h1:g5Xap1JfwKkUnZdn+S0L3SzBDpcTIYzZ5Qaag0YDkKQ= -github.com/go-openapi/loads v0.23.3/go.mod h1:NOH07zLajXo8y55hom0omlHWDVVvCwBM/S+csCK8LqA= -github.com/go-openapi/runtime v0.29.3 h1:h5twGaEqxtQg40ePiYm9vFFH1q06Czd7Ot6ufdK0w/Y= -github.com/go-openapi/runtime v0.29.3/go.mod h1:8A1W0/L5eyNJvKciqZtvIVQvYO66NlB7INMSZ9bw/oI= -github.com/go-openapi/spec v0.22.4 h1:4pxGjipMKu0FzFiu/DPwN3CTBRlVM2yLf/YTWorYfDQ= -github.com/go-openapi/spec v0.22.4/go.mod h1:WQ6Ai0VPWMZgMT4XySjlRIE6GP1bGQOtEThn3gcWLtQ= -github.com/go-openapi/strfmt v0.26.2 h1:ysjheCh4i1rmFEo2LanhELDNucNzfWTZhUDKgWWPaFM= -github.com/go-openapi/strfmt v0.26.2/go.mod h1:fXh1e449cyUn2NYuz+wb3wARBUdMl7qPEZwX00nqivY= -github.com/go-openapi/swag v0.25.5 h1:pNkwbUEeGwMtcgxDr+2GBPAk4kT+kJ+AaB+TMKAg+TU= -github.com/go-openapi/swag v0.25.5/go.mod h1:B3RT6l8q7X803JRxa2e59tHOiZlX1t8viplOcs9CwTA= -github.com/go-openapi/swag/cmdutils v0.25.5 h1:yh5hHrpgsw4NwM9KAEtaDTXILYzdXh/I8Whhx9hKj7c= -github.com/go-openapi/swag/cmdutils v0.25.5/go.mod h1:pdae/AFo6WxLl5L0rq87eRzVPm/XRHM3MoYgRMvG4A0= -github.com/go-openapi/swag/conv v0.28.0 h1:GtqqbyFe7vR5Y7ehxG9W6/OvrSFdf1OLeTGp40TqxH8= -github.com/go-openapi/swag/conv v0.28.0/go.mod h1:mbUE+mzctnhxi864m0Q07SpN8OowD9JhxmxuYvZZD/k= -github.com/go-openapi/swag/fileutils v0.25.5 h1:B6JTdOcs2c0dBIs9HnkyTW+5gC+8NIhVBUwERkFhMWk= -github.com/go-openapi/swag/fileutils v0.25.5/go.mod h1:V3cT9UdMQIaH4WiTrUc9EPtVA4txS0TOmRURmhGF4kc= -github.com/go-openapi/swag/jsonname v0.26.0 h1:gV1NFX9M8avo0YSpmWogqfQISigCmpaiNci8cGECU5w= -github.com/go-openapi/swag/jsonname v0.26.0/go.mod h1:urBBR8bZNoDYGr653ynhIx+gTeIz0ARZxHkAPktJK2M= -github.com/go-openapi/swag/jsonutils v0.25.5 h1:XUZF8awQr75MXeC+/iaw5usY/iM7nXPDwdG3Jbl9vYo= -github.com/go-openapi/swag/jsonutils v0.25.5/go.mod h1:48FXUaz8YsDAA9s5AnaUvAmry1UcLcNVWUjY42XkrN4= -github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.5 h1:SX6sE4FrGb4sEnnxbFL/25yZBb5Hcg1inLeErd86Y1U= -github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.5/go.mod h1:/2KvOTrKWjVA5Xli3DZWdMCZDzz3uV/T7bXwrKWPquo= -github.com/go-openapi/swag/loading v0.25.5 h1:odQ/umlIZ1ZVRteI6ckSrvP6e2w9UTF5qgNdemJHjuU= -github.com/go-openapi/swag/loading v0.25.5/go.mod h1:I8A8RaaQ4DApxhPSWLNYWh9NvmX2YKMoB9nwvv6oW6g= -github.com/go-openapi/swag/mangling v0.25.5 h1:hyrnvbQRS7vKePQPHHDso+k6CGn5ZBs5232UqWZmJZw= -github.com/go-openapi/swag/mangling v0.25.5/go.mod h1:6hadXM/o312N/h98RwByLg088U61TPGiltQn71Iw0NY= -github.com/go-openapi/swag/netutils v0.25.5 h1:LZq2Xc2QI8+7838elRAaPCeqJnHODfSyOa7ZGfxDKlU= -github.com/go-openapi/swag/netutils v0.25.5/go.mod h1:lHbtmj4m57APG/8H7ZcMMSWzNqIQcu0RFiXrPUara14= -github.com/go-openapi/swag/stringutils v0.25.5 h1:NVkoDOA8YBgtAR/zvCx5rhJKtZF3IzXcDdwOsYzrB6M= -github.com/go-openapi/swag/stringutils v0.25.5/go.mod h1:PKK8EZdu4QJq8iezt17HM8RXnLAzY7gW0O1KKarrZII= -github.com/go-openapi/swag/typeutils v0.28.0 h1:nRBKSBXjDgf01VDPB3fWeD9nQuhCOVeIYAkUx2tbkyY= -github.com/go-openapi/swag/typeutils v0.28.0/go.mod h1:Srm0xFNRZ1Y+vCxJclo5qzx8aj+1pAKda/YfFPrG0dQ= -github.com/go-openapi/swag/yamlutils v0.25.5 h1:kASCIS+oIeoc55j28T4o8KwlV2S4ZLPT6G0iq2SSbVQ= -github.com/go-openapi/swag/yamlutils v0.25.5/go.mod h1:Gek1/SjjfbYvM+Iq4QGwa/2lEXde9n2j4a3wI3pNuOQ= -github.com/go-openapi/testify/enable/yaml/v2 v2.4.1 h1:NZOrZmIb6PTv5LTFxr5/mKV/FjbUzGE7E6gLz7vFoOQ= -github.com/go-openapi/testify/enable/yaml/v2 v2.4.1/go.mod h1:r7dwsujEHawapMsxA69i+XMGZrQ5tRauhLAjV/sxg3Q= -github.com/go-openapi/testify/v2 v2.6.0 h1:5PKH2HE7YJ/LuRPQGvSxBRlFXNQhSetBLlGAgUEu3ug= -github.com/go-openapi/testify/v2 v2.6.0/go.mod h1:SgsVHtfooshd0tublTtJ50FPKhujf47YRqauXXOUxfw= -github.com/go-openapi/validate v0.25.2 h1:12NsfLAwGegqbGWr2CnvT65X/Q2USJipmJ9b7xDJZz0= -github.com/go-openapi/validate v0.25.2/go.mod h1:Pgl1LpPPGFnZ+ys4/hTlDiRYQdI1ocKypgE+8Q8BLfY= +github.com/go-openapi/analysis v0.26.2 h1:Q6wOwXW8mcVAkpDFMshj/F4PlK2Fx86tmLJjZW4vyEs= +github.com/go-openapi/analysis v0.26.2/go.mod h1:KAxGydTYCbs6dK9zjCMQ1AoEVD2RzSWEQNgmb64dRpI= +github.com/go-openapi/errors v0.22.8 h1:oP7sW7TWc3wFFjrzzj0nI83H2qMBkNjNfSd+XRejk/I= +github.com/go-openapi/errors v0.22.8/go.mod h1:BuUoHcYrU6E7V9gfj1I5wLQqgtIHnup/alXZ8KdgQ0w= +github.com/go-openapi/jsonpointer v1.0.0 h1:kR9tHqY0CtZaOPVFm622dPVNhrvYpwr4uCxgL3h1H8s= +github.com/go-openapi/jsonpointer v1.0.0/go.mod h1:Z3rw7dWu1p9IgitXCFamSlA5lmDiklEB6vkaxcNZW5Y= +github.com/go-openapi/jsonreference v1.0.1 h1:4zJ7AmYDKNmD3aSpfPnFNCFA5E80/xMHUNKgydaLh38= +github.com/go-openapi/jsonreference v1.0.1/go.mod h1:dYplQXa6p5lXprLcJ8LE2iU7vNpXsAHDQ5ZAgL+Qx3A= +github.com/go-openapi/loads v0.25.2 h1:+uNsDlRQfYtZTrh+3pdwampcAqZVPuBJW0IA82aZHII= +github.com/go-openapi/loads v0.25.2/go.mod h1:RXsfJEQGGNv4uw8u/KdmF8Vh8OR4fTfs9ZO4QrzNohA= +github.com/go-openapi/runtime v0.33.1 h1:jCvhI+wAdsn29byy+RgcPcg+j39YT6E304QOE/WqIVk= +github.com/go-openapi/runtime v0.33.1/go.mod h1:Dl5SMVRnJz+d8bX6Y1zxy0QKpqe/ysvVeUEh1nCpEZ4= +github.com/go-openapi/runtime/server-middleware v0.33.1 h1:IAeKbwWnBnpsYTpuPVS8t73ZrPpKvRZnK2iJ2KJGUV0= +github.com/go-openapi/runtime/server-middleware v0.33.1/go.mod h1:2Gej5fDxqeJxY+w38vxXYW0BgFASfgBsJ5rXwN1Fseg= +github.com/go-openapi/spec v1.0.0 h1:JtB/GHOj+eetjse6YvxqLze88oEekl/4uPBethvzRrA= +github.com/go-openapi/spec v1.0.0/go.mod h1:boj1PRhqS0x5jylgcNp9BRWxenphrh7vVNYZ90IRCoo= +github.com/go-openapi/strfmt v0.27.0 h1:kbcTeaD9TXuXD0hhMXzuYa1sdTo6+dWGvwjW93E80IM= +github.com/go-openapi/strfmt v0.27.0/go.mod h1:s/qhDqfY72irigXUGJmtgid2Rm+3tnz3k8hZaRmvWYc= +github.com/go-openapi/swag v0.29.1 h1:C6EeWzUwQtcWEhE9eqBdUubGXxhWY4PlzHMLD7kLaiQ= +github.com/go-openapi/swag v0.29.1/go.mod h1:BzxEXKiPlSXRsRTv1KSBF/BpGKHxA/YciCnr4tv9bvA= +github.com/go-openapi/swag/cmdutils v0.29.1 h1:3DorPGfUdE80BogKY22EzoHBcHMrkVomZMoV7kS4ANY= +github.com/go-openapi/swag/cmdutils v0.29.1/go.mod h1:Sm1MVFMkF6guJJ+pQqHnQA3N0j9qALV3NxzDSv6bETM= +github.com/go-openapi/swag/conv v0.29.1 h1:AC4Eh/5c/eUDOUCzzsRC9ghmFgOSBHeRMGIngY0ZUGA= +github.com/go-openapi/swag/conv v0.29.1/go.mod h1:S1X7/ZrBEZOC0Wc8AGxjbcGS92l3WEjA7aPtpl+RaqM= +github.com/go-openapi/swag/fileutils v0.29.1 h1:ZcPzMceVhU1WPbK6N1G6sNQKdd1CWJlf3cA08UHuoM0= +github.com/go-openapi/swag/fileutils v0.29.1/go.mod h1:/wofKYckbtRl2p3+EwQsosie5CT1B38+dQ+PS579BzI= +github.com/go-openapi/swag/jsonutils v0.29.1 h1:AFCxs0eQZ24/QyfhVHM2t49rMz7Vv3XCsZQI6yrNy+c= +github.com/go-openapi/swag/jsonutils v0.29.1/go.mod h1:u3+sCfJpttDpcmS5kpm0yxL6GK0eWgODsx8Yw8fcqNM= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.29.1 h1:BiiXE31Bx9SfpsMmOQj5KYpUhTZBpLVriVhJDuLuY2o= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.29.1/go.mod h1:julgTUKZ9/D0j6O7GKajmRs+812FWxQg/mMpGunWSjg= +github.com/go-openapi/swag/loading v0.29.1 h1:FCv5fG8UhTdDJa2R7w+5O9Ekpcbw7tt0nFWvmDKGBjc= +github.com/go-openapi/swag/loading v0.29.1/go.mod h1:N0ESuem4p2oedKal8EJhciqnJ9Q9Wmt83L1CRB3Fouw= +github.com/go-openapi/swag/mangling v0.29.1 h1:lHALtvYCdxVnRl4GrHmFPwfBTZYIObqdGNSKyu/8D6I= +github.com/go-openapi/swag/mangling v0.29.1/go.mod h1:SAop9pB7PUjQ/CGCNf/JmCKTRK+GDO+RqE9UHqC/N6s= +github.com/go-openapi/swag/netutils v0.29.1 h1:IjIvdEP5duKcghFqJEPSUraRnkKYHoM65kTluTu+Jb4= +github.com/go-openapi/swag/netutils v0.29.1/go.mod h1:DUde7x4Bx00k5jYl2AdRpNAO0m7atUvD2x6X+bWkbno= +github.com/go-openapi/swag/pools v0.29.1 h1:NRogYxdEW9SjRM4mkAOji9iefO4MRXq3p/ZJcoQbUKg= +github.com/go-openapi/swag/pools v0.29.1/go.mod h1:leDcaghjkRAhCuCRv9NfJU5f0mjoU3cT/XZObhMk3pc= +github.com/go-openapi/swag/stringutils v0.29.1 h1:1ykunK7iJQk1uOO7+oUH1ukbsK85fFCOiCFMOVSY+F0= +github.com/go-openapi/swag/stringutils v0.29.1/go.mod h1:7fSqZ+z8Qc0tOfAAK0jVa5qFGrnIlRi6n7NeGGrr1vc= +github.com/go-openapi/swag/typeutils v0.29.1 h1:Nzv9nhnlLCRBPQqfOX+7lB6Guju370or8StT+lIOf6M= +github.com/go-openapi/swag/typeutils v0.29.1/go.mod h1:hxpgDZJVBkBsi/d3MIUosafoFdE5exaQRmVp0zwu3YE= +github.com/go-openapi/swag/yamlutils v0.29.1 h1:69w3tsBajm7MR/fejLy7HD/3J68Ys1SeeZMEzZ3w2sk= +github.com/go-openapi/swag/yamlutils v0.29.1/go.mod h1:rgsp3vT/QdWzKwn43CigDwjOGIenPyTZMKnxEM8jZOA= +github.com/go-openapi/testify/enable/yaml/v2 v2.7.0 h1:wPW6YRgx3+SID1yUy/Xwa17L8kFEaEKod2VRbJDZNUs= +github.com/go-openapi/testify/enable/yaml/v2 v2.7.0/go.mod h1:mI1M88etYbc3PhgHsWQK2kwvNwW5aGFqMPbmib+SGIs= +github.com/go-openapi/testify/v2 v2.7.0 h1:bycOreEj6wfBvijg3YFogZ/sFjTCDmQnwSodSzHa3X8= +github.com/go-openapi/testify/v2 v2.7.0/go.mod h1:SgsVHtfooshd0tublTtJ50FPKhujf47YRqauXXOUxfw= +github.com/go-openapi/validate v0.26.5 h1:Vm02dSmhevDx/4v4m8KAtMwffHGfq9wRLqICeebE/D4= +github.com/go-openapi/validate v0.26.5/go.mod h1:2VVi2kpxSvInv/OOI6echOcYddydjXOxJvkHQ5VsoZw= github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo= github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU= @@ -296,28 +296,26 @@ github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/ github.com/google/martian/v3 v3.3.3 h1:DIhPTQrbPkgs2yJYdXU/eNACCG5DVQjySNRNlflZ9Fc= github.com/google/martian/v3 v3.3.3/go.mod h1:iEPrYcgCF7jA9OtScMFQyAlZZ4YXTKEtJ1E6RWzmBA0= github.com/google/pprof v0.0.0-20240227163752-401108e1b7e7/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik= -github.com/google/pprof v0.0.0-20260507013755-92041b743c96 h1:YDDnaZ9afWajDboPMt9Vikqca/yWAX7KAxVzb4lJU1M= -github.com/google/pprof v0.0.0-20260507013755-92041b743c96/go.mod h1:MxpfABSjhmINe3F1It9d+8exIHFvUqtLIRCdOGNXqiI= +github.com/google/pprof v0.0.0-20260709232956-b9395ee17fa0 h1:du0WGc8xSKq/++e0cglxhS/mXVqsR7+c7jLEi5Vqduw= +github.com/google/pprof v0.0.0-20260709232956-b9395ee17fa0/go.mod h1:MxpfABSjhmINe3F1It9d+8exIHFvUqtLIRCdOGNXqiI= github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/googleapis/enterprise-certificate-proxy v0.3.15 h1:xolVQTEXusUcAA5UgtyRLjelpFFHWlPQ4XfWGc7MBas= -github.com/googleapis/enterprise-certificate-proxy v0.3.15/go.mod h1:vqVt9yG9480NtzREnTlmGSBmFrA+bzb0yl0TxoBQXOg= -github.com/googleapis/gax-go/v2 v2.22.0 h1:PjIWBpgGIVKGoCXuiCoP64altEJCj3/Ei+kSU5vlZD4= -github.com/googleapis/gax-go/v2 v2.22.0/go.mod h1:irWBbALSr0Sk3qlqb9SyJ1h68WjgeFuiOzI4Rqw5+aY= -github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= -github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= +github.com/googleapis/enterprise-certificate-proxy v0.3.18 h1:hvVi34VucdrV1IIsiWuqYM8kutw/92MxNEFxCJZEh0k= +github.com/googleapis/enterprise-certificate-proxy v0.3.18/go.mod h1:rSEsBUemEBZEexP2y6jPp16LUmUbjmSbcPMQizR0o4k= +github.com/googleapis/gax-go/v2 v2.23.0 h1:Tchl7qkvE7Ip3y+ztvNufYFvkfqTe7NfLTYGIdJRLuE= +github.com/googleapis/gax-go/v2 v2.23.0/go.mod h1:rBQKOVJCdb8IFEzg+FCwlt1LP/xMDGuqUXhUG+XMXEg= github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo= github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA= github.com/grafana/regexp v0.0.0-20250905093917-f7b3be9d1853 h1:cLN4IBkmkYZNnk7EAJ0BHIethd+J6LqxFNw5mSiI2bM= github.com/grafana/regexp v0.0.0-20250905093917-f7b3be9d1853/go.mod h1:+JKpmjMGhpgPL+rXZ5nsZieVzvarn86asRlBg4uNGnk= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 h1:5VipnvEpbqr2gA2VbM+nYVbkIF28c5ZQfqCBQ5g2xfk= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0/go.mod h1:Hyl3n6Twe1hvtd9XUXDec4pTvgMSEixRuQKPTMH2bNs= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.30.0 h1:/Tnpcb2E0Pz/tN9s3bfEY2Q8ePCEX9iuS+cneUwncnw= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.30.0/go.mod h1:zOBXOsUaBSjKgmH4OGzV1esUpR3oUSCPYVd2cUBjKYY= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -362,8 +360,8 @@ github.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh6 github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs= github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY= github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc= -github.com/jedib0t/go-pretty/v6 v6.7.8 h1:BVYrDy5DPBA3Qn9ICT+PokP9cvCv1KaHv2i+Hc8sr5o= -github.com/jedib0t/go-pretty/v6 v6.7.8/go.mod h1:YwC5CE4fJ1HFUDeivSV1r//AmANFHyqczZk+U6BDALU= +github.com/jedib0t/go-pretty/v6 v6.8.3 h1:yVSk5aemoYHCvcrtqyXklwqcgHQIQzmy/oUzFlmffSQ= +github.com/jedib0t/go-pretty/v6 v6.8.3/go.mod h1:YwC5CE4fJ1HFUDeivSV1r//AmANFHyqczZk+U6BDALU= github.com/jessevdk/go-flags v1.6.1 h1:Cvu5U8UGrLay1rZfv/zP7iLpSHGUZ/Ou68T0iX1bBK4= github.com/jessevdk/go-flags v1.6.1/go.mod h1:Mk8T1hIAWpOiJiHa9rJASDK2UGWji0EuPGBnNLMooyc= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= @@ -376,11 +374,11 @@ github.com/keybase/go-keychain v0.0.1/go.mod h1:PdEILRW3i9D8JcdM+FmY6RwkHGnhHxXw github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.14.4/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= -github.com/klauspost/compress v1.18.7 h1:aUyZsS4kH3QTKurYhAOwAHxllVPnOthb3vPfnF1Ehjw= -github.com/klauspost/compress v1.18.7/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= +github.com/klauspost/compress v1.20.0 h1:a3C1ke2ohxFymNlb2HWAHjDeKCI90scRskErZkR0ezA= +github.com/klauspost/compress v1.20.0/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI= github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= -github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= -github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +github.com/klauspost/cpuid/v2 v2.4.0 h1:S6Hrbc7+ywsr0r+RLapfGBHfyefhCTwEh3A0tV913Dw= +github.com/klauspost/cpuid/v2 v2.4.0/go.mod h1:19jmZ9mjzoF//ddRSUsv0zfBTJWh3QJh9FNxZTMrGxU= github.com/klauspost/crc32 v1.3.0 h1:sSmTt3gUt81RP655XGZPElI0PelVTZ6YwCRnPSupoFM= github.com/klauspost/crc32 v1.3.0/go.mod h1:D7kQaZhnkX/Y0tstFGf8VUzv2UofNGqCjnC3zdHB0Hw= github.com/klauspost/filepathx v1.1.1 h1:201zvAsL1PhZvmXTP+QLer3AavWrO3U1NILWpniHK4w= @@ -405,18 +403,16 @@ github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+ github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80/go.mod h1:imJHygn/1yfhB7XSJJKlFZKl/J+dCPAknuiaGOshXAs= github.com/lestrrat-go/blackmagic v1.0.4 h1:IwQibdnf8l2KoO+qC3uT4OaTWsW7tuRQXy9TRN9QanA= github.com/lestrrat-go/blackmagic v1.0.4/go.mod h1:6AWFyKNNj0zEXQYfTMPfZrAXUWUfTIZ5ECEUEJaijtw= -github.com/lestrrat-go/dsig v1.0.0 h1:OE09s2r9Z81kxzJYRn07TFM9XA4akrUdoMwr0L8xj38= -github.com/lestrrat-go/dsig v1.0.0/go.mod h1:dEgoOYYEJvW6XGbLasr8TFcAxoWrKlbQvmJgCR0qkDo= +github.com/lestrrat-go/dsig v1.4.0 h1:g7LUjK8cT74A5DzBXJI5HzsJuLhoYN0Wzj4nuOMIrH8= +github.com/lestrrat-go/dsig v1.4.0/go.mod h1:I8Nddg/vN2cUl/h8N7SRRApLnNNeyZPIqLYpvpOtGGo= github.com/lestrrat-go/dsig-secp256k1 v1.0.0 h1:JpDe4Aybfl0soBvoVwjqDbp+9S1Y2OM7gcrVVMFPOzY= github.com/lestrrat-go/dsig-secp256k1 v1.0.0/go.mod h1:CxUgAhssb8FToqbL8NjSPoGQlnO4w3LG1P0qPWQm/NU= github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE= github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E= -github.com/lestrrat-go/httprc/v3 v3.0.1 h1:3n7Es68YYGZb2Jf+k//llA4FTZMl3yCwIjFIk4ubevI= -github.com/lestrrat-go/httprc/v3 v3.0.1/go.mod h1:2uAvmbXE4Xq8kAUjVrZOq1tZVYYYs5iP62Cmtru00xk= -github.com/lestrrat-go/jwx/v3 v3.0.12 h1:p25r68Y4KrbBdYjIsQweYxq794CtGCzcrc5dGzJIRjg= -github.com/lestrrat-go/jwx/v3 v3.0.12/go.mod h1:HiUSaNmMLXgZ08OmGBaPVvoZQgJVOQphSrGr5zMamS8= -github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU= -github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= +github.com/lestrrat-go/httprc/v3 v3.0.6 h1:4FpLQ18KK/ypPbVU3NLWJNRvH3kcYiqKqWfKGqNWxxI= +github.com/lestrrat-go/httprc/v3 v3.0.6/go.mod h1:mSMtkZW92Z98M5YoNNztbRGxbXHql7tSitCvaxvo9l0= +github.com/lestrrat-go/jwx/v3 v3.2.0 h1:Jb3zBASTSZXz7gzzSAfYqxXF8KejvKC4xWoePLQqXCA= +github.com/lestrrat-go/jwx/v3 v3.2.0/go.mod h1:38vQ8iWKq3qRSbilbzvzdQPuywhowwuR03lhkYskyrw= github.com/lestrrat-go/option/v2 v2.0.0 h1:XxrcaJESE1fokHy3FpaQ/cXW8ZsIdWcdFzzLOcID3Ss= github.com/lestrrat-go/option/v2 v2.0.0/go.mod h1:oSySsmzMoR0iRzCDCaUfsCzxQHUEuhOViQObyy7S6Vg= github.com/lib/pq v1.10.4/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= @@ -424,10 +420,10 @@ github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lithammer/shortuuid/v4 v4.2.0 h1:LMFOzVB3996a7b8aBuEXxqOBflbfPQAiVzkIcHO0h8c= github.com/lithammer/shortuuid/v4 v4.2.0/go.mod h1:D5noHZ2oFw/YaKCfGy0YxyE7M0wMbezmMjPdhyEFe6Y= -github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag= -github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= -github.com/lufia/plan9stats v0.0.0-20260216142805-b3301c5f2a88 h1:PTw+yKnXcOFCR6+8hHTyWBeQ/P4Nb7dd4/0ohEcWQuM= -github.com/lufia/plan9stats v0.0.0-20260216142805-b3301c5f2a88/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg= +github.com/lucasb-eyer/go-colorful v1.4.1 h1:1EO+WB73+EH8EVbzlrG3KLAfEypQWVHIBqlTf+2hNss= +github.com/lucasb-eyer/go-colorful v1.4.1/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/lufia/plan9stats v0.0.0-20260802145828-341c2f0c90b5 h1:eveIIGn4BGM3qknO74omf6HYr30/exH+eVUTuAgwjZ0= +github.com/lufia/plan9stats v0.0.0-20260802145828-341c2f0c90b5/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= @@ -446,13 +442,13 @@ github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzp github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= -github.com/mattn/go-runewidth v0.0.21 h1:jJKAZiQH+2mIinzCJIaIG9Be1+0NR+5sz/lYEEjdM8w= -github.com/mattn/go-runewidth v0.0.21/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= +github.com/mattn/go-runewidth v0.0.29 h1:3oGF3R/S2N9DQ3ptftzVIvg2eicmojCzlwBEmqEPDfQ= +github.com/mattn/go-runewidth v0.0.29/go.mod h1:3qAiGCV4Koz/yuveO58qUefmUTRm8r0IGEXZ9jeHp/8= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= -github.com/miekg/dns v1.1.72 h1:vhmr+TF2A3tuoGNkLDFK9zi36F2LS+hKTRW0Uf8kbzI= -github.com/miekg/dns v1.1.72/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs= +github.com/miekg/dns v1.1.73 h1:uhT8nJxmTrPJYClxVxTCX+CVn6qnzSiybRk72Z6DgrE= +github.com/miekg/dns v1.1.73/go.mod h1:RW2Obtfd5NZHvOFe3zYG0W8koWOQtAzyHaLo8vASBuQ= github.com/minio/cli v1.24.2 h1:J+fCUh9mhPLjN3Lj/YhklXvxj8mnyE/D6FpFduXJ2jg= github.com/minio/cli v1.24.2/go.mod h1:bYxnK0uS629N3Bq+AOZZ+6lwF77Sodk4+UL9vNuXhOY= github.com/minio/colorjson v1.0.8 h1:AS6gEQ1dTRYHmC4xuoodPDRILHP/9Wz5wYUGDQfPLpg= @@ -478,10 +474,12 @@ 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.0.99 h1:2vH/byrwUkIpFQFOilvTfaUpvAX3fEFhEzO+DR3DlCE= -github.com/minio/minio-go/v7 v7.0.99/go.mod h1:EtGNKtlX20iL2yaYnxEigaIvj0G0GwSDnifnG8ClIdw= -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/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.10.1 h1:grrK8SwRKbkNFE6qG7WAvFGH09bB46d5teOOtKfQ14s= +github.com/minio/mux v1.10.1/go.mod h1:INYT4sMSTJy0QWUEA/E2DZNxJ5sAxIwbnyZjkzNFRfE= +github.com/minio/pkg/v3 v3.6.1 h1:gaNT80BS/iuIany5ylTkVmfN4s6UYY30OtImFv4GQA8= +github.com/minio/pkg/v3 v3.6.1/go.mod h1:fYlexVD0GMD0XNeBHeefFI6YBE0Oo8oDbDPWm3Jd68I= github.com/minio/selfupdate v0.6.0 h1:i76PgT0K5xO9+hjzKcacQtO7+MjJ4JKA8Ak8XQ9DDwU= github.com/minio/selfupdate v0.6.0/go.mod h1:bO02GTIPCMQFTEvE5h4DjYB58bCoZ35XLeBf0buTDdM= github.com/minio/simdjson-go v0.4.5 h1:r4IQwjRGmWCQ2VeMc7fGiilu1z5du0gJ/I/FsKwgo5A= @@ -539,59 +537,57 @@ github.com/ncw/directio v1.0.5 h1:JSUBhdjEvVaJvOoyPAbcW0fnd0tvRXD76wEfZ1KcQz4= github.com/ncw/directio v1.0.5/go.mod h1:rX/pKEYkOXBGOggmcyJeJGloCkleSvphPx2eV3t6ROk= github.com/nsqio/go-nsq v1.1.0 h1:PQg+xxiUjA7V+TLdXw7nVrJ5Jbl3sN86EhGCQj4+FYE= github.com/nsqio/go-nsq v1.1.0/go.mod h1:vKq36oyeVXgsS5Q8YEO7WghqidAVXQlcFxzQbQTuDEY= -github.com/oklog/ulid/v2 v2.1.1 h1:suPZ4ARWLOJLegGFiZZ1dFAkqzhMjL3J1TzI+5wHz8s= -github.com/oklog/ulid/v2 v2.1.1/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ= +github.com/oklog/ulid/v2 v2.1.2 h1:IEclFb9JNvzYA6MW2SCxbLzcHTVsfqm3PrqGQJH5zec= +github.com/oklog/ulid/v2 v2.1.2/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ= github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0= github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= -github.com/pgsty/mc v0.0.0-20260801042411-ad10a2a10b76 h1:UIlUuz0LQKw4QlAljhv7nPDDFC1+n+e0iED7rWZrgZ8= -github.com/pgsty/mc v0.0.0-20260801042411-ad10a2a10b76/go.mod h1:cTbS+9jGR4Qs7xTf5DEhmCTbzcDWrKMs8ZmTUnCU49E= -github.com/pgsty/silo-console v0.0.0-20260806061103-72fc0a5ea52a h1:JfEQJkBdTwCXSrCv8P0R5tMpzxxhSdUTvg54mHwzxpA= -github.com/pgsty/silo-console v0.0.0-20260806061103-72fc0a5ea52a/go.mod h1:7J8wCQsNT5S7GqCHnQqgj0T7Nagp1fklJhfBJI+v0XI= -github.com/pgsty/silo-pkg/v3 v3.11.0 h1:wjN5d+tWD8Twq+e7k/KBBVhnWXC8xTIlfTcnGIKkmjc= -github.com/pgsty/silo-pkg/v3 v3.11.0/go.mod h1:E2AB4oOgfDeb9In1KDBTrn9wzfvr0WzoPkbXW7wbwBQ= +github.com/pgsty/mc v0.0.0-20260903063637-a2ef95c035d9 h1:kJkqK0hJrmvdTOPiI98HoPmET0SHYI9+ytaTRzQdGAs= +github.com/pgsty/mc v0.0.0-20260903063637-a2ef95c035d9/go.mod h1:+j12ENu7ggWIaBY4nakGABVmkjfJgogzijHIbaJFSOk= +github.com/pgsty/silo-console v0.0.0-20260908011343-b39a84ada5e8 h1:B270uCa2FLThDqTIJ+TzKksOWAfjmik5mcugwZFVn4s= +github.com/pgsty/silo-console v0.0.0-20260908011343-b39a84ada5e8/go.mod h1:lInRIXU5jctVcagYHkvDuFRUYm/eCOJ1VNt1tciTHso= +github.com/pgsty/silo-pkg/v3 v3.13.2 h1:Clw11c/J54Tx6pijNCWtXiC7e0fwP/f5Tgeb6fsXg2w= +github.com/pgsty/silo-pkg/v3 v3.13.2/go.mod h1:0GmaDA0ArQ8bkAI/obiSNTBQzdgBu6W0e0olDCbyXXo= github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM= github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM= -github.com/pierrec/lz4/v4 v4.1.26 h1:GrpZw1gZttORinvzBdXPUXATeqlJjqUG/D87TKMnhjY= -github.com/pierrec/lz4/v4 v4.1.26/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4= +github.com/pierrec/lz4/v4 v4.1.29 h1:CDQY6qZOLI4DW0Nx6R1vRrifrCeQHnNXkMb0hZWXFjg= +github.com/pierrec/lz4/v4 v4.1.29/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/sftp v1.13.10 h1:+5FbKNTe5Z9aspU88DPIKJ9z2KZoaGCu6Sr6kKR/5mU= -github.com/pkg/sftp v1.13.10/go.mod h1:bJ1a7uDhrX/4OII+agvy28lzRvQrmIQuaHrcI1HbeGA= +github.com/pkg/sftp v1.13.11 h1:0N92SLTB8JqASJB14ZLHHzFnBV8mG9zw4K7jghEFWuE= +github.com/pkg/sftp v1.13.11/go.mod h1:uNkH9roSXglNJqM+glJJi+TQXQUm0fXFWqCFmT8hsN0= github.com/pkg/xattr v0.4.12 h1:rRTkSyFNTRElv6pkA3zpjHpQ90p/OdHQC1GmGh1aTjM= github.com/pkg/xattr v0.4.12/go.mod h1:di8WF84zAKk8jzR1UBTEWh9AUlIZZ7M/JNt8e9B6ktU= github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo= github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= -github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/posener/complete v1.2.3 h1:NP0eAhjcjImqslEwo/1hq7gpajME0fTLTezBKDqfXqo= github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s= -github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU= -github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= +github.com/power-devops/perfstat v0.0.0-20260805114148-88456608a4f6 h1:jL3a8soXdzuTCcRnKhOmtcsVOObdDTFf4O2B403HPRU= +github.com/power-devops/perfstat v0.0.0-20260805114148-88456608a4f6/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/prometheus/client_golang v0.9.2/go.mod h1:OsXs2jCmiKlQ1lTBmv21f2mNfw4xf/QclQDMrYNZzcM= -github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= -github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= +github.com/prometheus/client_golang v1.24.1 h1:JnJkREXzWxUdCuPFpIWZiPispT9xVV59uiuyR2bPlnU= +github.com/prometheus/client_golang v1.24.1/go.mod h1:F+oSRECHg4sse5ucfYpYDeIv/hu68Zo0uoHKetWnzcE= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= -github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= -github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= +github.com/prometheus/client_model v0.6.3 h1:O0jaTVAYNxTHYInEPFJt5I3+sN8zqBtVMPTB1qyxiEo= +github.com/prometheus/client_model v0.6.3/go.mod h1:gpN5P9S7Rr6Yr92PiQ+Ixvhf6JZEkF1dnxsYL2aPBEM= github.com/prometheus/common v0.0.0-20181126121408-4724e9255275/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= -github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4= -github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw= +github.com/prometheus/common v0.71.0 h1:9KDAKb7Mj3HEVKyFCK6Dc/HIwlBzZIN2l7/lrHl3KK8= +github.com/prometheus/common v0.71.0/go.mod h1:CLJ5H8TEsGX8bl31BdMkfhIZ+QmZ9tBPPotUxUbfcmk= github.com/prometheus/procfs v0.0.0-20181204211112-1dc9a6cbc91a/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= -github.com/prometheus/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEycfc= -github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo= +github.com/prometheus/procfs v0.22.0 h1:6q9+/JL9IKAPbCmBrv9n5O5Ty3NKnciV5X7YGw0oics= +github.com/prometheus/procfs v0.22.0/go.mod h1:CvmFr/GVhIjIvWJZW3tgkODBQMRIf0EyWMQLHCHab58= github.com/prometheus/prom2json v1.5.0 h1:WIcAOjLE1x476W3dUlmTL6E/e98CgVGuwwYusl6MPP8= github.com/prometheus/prom2json v1.5.0/go.mod h1:xPp6KDhCA30btxmqEfg/K3DAwgTIkp7TJKi4+7jaYd8= -github.com/prometheus/prometheus v0.311.3 h1:3IrVxQv6v5i/ZCGi6OrYeBhtCwaPTn6Z3DYruXoYm3M= -github.com/prometheus/prometheus v0.311.3/go.mod h1:gjsCxTKtHO1Q8T9333u1s+lUR1OjPyM7ruuGH8RvVyo= +github.com/prometheus/prometheus v0.314.0 h1:YjsimqsIi6/mOtzZcrPEYUALO6zpfaht9O5sXqDz2vg= +github.com/prometheus/prometheus v0.314.0/go.mod h1:zjg3pMTAkY0/JG8jy/h8/YgSQUVB+aCXMhUqN6l64jg= github.com/puzpuzpuz/xsync/v3 v3.5.1 h1:GJYJZwO6IdxN/IKbneznS6yPkVC+c3zyY/j19c++5Fg= github.com/puzpuzpuz/xsync/v3 v3.5.1/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA= github.com/rabbitmq/amqp091-go v1.10.0 h1:STpn5XsHlHGcecLmMFCtg7mqq0RnD+zFr4uzukfVhBw= @@ -604,8 +600,8 @@ github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rjeczalik/notify v0.9.3 h1:6rJAzHTGKXGj76sbRgDiDcYj/HniypXmSJo1SWakZeY= github.com/rjeczalik/notify v0.9.3/go.mod h1:gF3zSOrafR9DQEWSE8TjfI9NkooDxbyT4UgRGKZA0lc= -github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= -github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/rogpeppe/go-internal v1.16.0 h1:O9DK+vNMDVGLr2BeZqmpLeMjiMNkuXfcqntWbZV6S5g= +github.com/rogpeppe/go-internal v1.16.0/go.mod h1:DrUVZyrJU+txYW5/1kwtXQSMFio52ZOxX7yM1VHvnxs= github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA= github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU= @@ -631,51 +627,53 @@ github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb6 github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= -github.com/spiffe/go-spiffe/v2 v2.6.0 h1:l+DolpxNWYgruGQVV0xsfeya3CsC7m8iBzDnMpsbLuo= -github.com/spiffe/go-spiffe/v2 v2.6.0/go.mod h1:gm2SeUoMZEtpnzPNs2Csc0D/gX33k1xIx7lEzqblHEs= +github.com/spiffe/go-spiffe/v2 v2.7.0 h1:uXe1MflJoHw58wAUvxVlcM7WpKtijWG7I1UidcGh6g4= +github.com/spiffe/go-spiffe/v2 v2.7.0/go.mod h1:47Q0Q9/AqGha8QLHp+kxpH4Wca7X7EnOtlIJy3mxZ3U= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= -github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= -github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= -github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE= +github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg= +github.com/tidwall/gjson v1.19.0 h1:xwxm7n691Uf3u5OFjzngavjGTh55KX5q/9w9xHW88JU= +github.com/tidwall/gjson v1.19.0/go.mod h1:V37/opeE/JbLUOfH0QTXiNez2l0RUjYUhpT4szFQAfc= github.com/tidwall/match v1.2.0 h1:0pt8FlkOwjN2fPt4bIl4BoNxb98gGHN2ObFEDkrfZnM= github.com/tidwall/match v1.2.0/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= -github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tinylib/msgp v1.6.4 h1:mOwYbyYDLPj35mkA2BjjYejgJk9BuHxDdvRnb6v2ZcQ= github.com/tinylib/msgp v1.6.4/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA= -github.com/tklauser/go-sysconf v0.3.16 h1:frioLaCQSsF5Cy1jgRBrzr6t502KIIwQ0MArYICU0nA= -github.com/tklauser/go-sysconf v0.3.16/go.mod h1:/qNL9xxDhc7tx3HSRsLWNnuzbVfh3e7gh/BmM179nYI= -github.com/tklauser/numcpus v0.11.0 h1:nSTwhKH5e1dMNsCdVBukSZrURJRoHbSEQjdEbY+9RXw= -github.com/tklauser/numcpus v0.11.0/go.mod h1:z+LwcLq54uWZTX0u/bGobaV34u6V7KNlTZejzM6/3MQ= +github.com/tklauser/go-sysconf v0.4.0 h1:7H0uAN+7RkwWRaxhYXDLqa5V3LPrJeV8wmD9dRUgPQU= +github.com/tklauser/go-sysconf v0.4.0/go.mod h1:8mTNWyog7H+MpKijp4VmKJAd2bbYQ2zuUwkYRbUArPI= +github.com/tklauser/numcpus v0.12.0 h1:NR85qdvHA9pFse3x3weVZ0r0ST8R6l5RHbZrlRaqob4= +github.com/tklauser/numcpus v0.12.0/go.mod h1:ABHeXzJnr/qqwguhClkZKT1/8VABcYrsyUiUGobwWJg= github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= github.com/unrolled/secure v1.17.0 h1:Io7ifFgo99Bnh0J7+Q+qcMzWM6kaDPCA5FroFZEdbWU= github.com/unrolled/secure v1.17.0/go.mod h1:BmF5hyM6tXczk3MpQkFf1hpKSRqCyhqcbiQtiAF7+40= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= -github.com/valyala/fastjson v1.6.4 h1:uAUNq9Z6ymTgGhcm0UynUAB6tlbakBrz6CQFax3BXVQ= -github.com/valyala/fastjson v1.6.4/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY= -github.com/vbauerster/mpb/v8 v8.12.0 h1:+gneY3ifzc88tKDzOtfG8k8gfngCx615S2ZmFM4liWg= -github.com/vbauerster/mpb/v8 v8.12.0/go.mod h1:V02YIuMVo301Y1VE9VtZlD8s84OMsk+EKN6mwvf/588= +github.com/valyala/fastjson v1.6.10 h1:/yjJg8jaVQdYR3arGxPE2X5z89xrlhS0eGXdv+ADTh4= +github.com/valyala/fastjson v1.6.10/go.mod h1:e6FubmQouUNP73jtMLmcbxS6ydWIpOfhz34TSfO3JaE= +github.com/vbauerster/cupwriter v0.0.4 h1:9sBPe0uXWLZuWQU5lqVbhyFlxX6c09asST/YfatFAys= +github.com/vbauerster/cupwriter v0.0.4/go.mod h1:IFyzS6Xis5dnBH/rdAhrnuzg3c+KkUqEN6yE8lhJlDw= +github.com/vbauerster/mpb/v8 v8.16.1 h1:gNYmwMip9xRWNGAiblZOgUNXWeU2P0NIGd5x0f8ffbc= +github.com/vbauerster/mpb/v8 v8.16.1/go.mod h1:gnU8zNF/JWltFepqwko/ulMEUIDrydIq7T4UdMN26Nw= github.com/xdg/scram v1.0.5 h1:TuS0RFmt5Is5qm9Tm2SoD89OPqe4IRiFtyFY4iwWXsw= github.com/xdg/scram v1.0.5/go.mod h1:lB8K/P019DLNhemzwFU4jHLhdvlE6uDZjXFejJXr49I= github.com/xdg/stringprep v1.0.3 h1:cmL5Enob4W83ti/ZHuZLuKD/xqJfus4fVPwE+/BDm+4= github.com/xdg/stringprep v1.0.3/go.mod h1:Jhud4/sHMO4oL310DaZAKk9ZaJ08SJfe+sJh0HrGL1Y= -github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= -github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= +github.com/xo/terminfo v1.0.0 h1:2ZpYzqWzyyytjk3TP6aJVDhkMAkc99/1xKQdA3TDTBY= +github.com/xo/terminfo v1.0.0/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -688,34 +686,34 @@ github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs= github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s= go.etcd.io/bbolt v1.3.6 h1:/ecaJf0sk1l4l6V4awd65v2C3ILy7MSj+s/x1ADCIMU= go.etcd.io/bbolt v1.3.6/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4= -go.etcd.io/etcd/api/v3 v3.6.9 h1:UA7iKfEW1AzgihcBSGXci2kDGQiokSq41F9HMCI/RTI= -go.etcd.io/etcd/api/v3 v3.6.9/go.mod h1:csEk/qTfxKL36NqJdU15Tgtl65A8dyEY2BYo7PRsIwk= -go.etcd.io/etcd/client/pkg/v3 v3.6.9 h1:T8nuk8Lz64C+Hzb0coBFLMSlVSQZBpAtFk46swdM1DA= -go.etcd.io/etcd/client/pkg/v3 v3.6.9/go.mod h1:WEy3PpwbbEBVRdh1NVJYsuUe/8eyI21PNJRazeD8z/Y= -go.etcd.io/etcd/client/v3 v3.6.9 h1:3X555hQXmhRr27O37wls53g68CpUiPOiHXrZfz2Al+o= -go.etcd.io/etcd/client/v3 v3.6.9/go.mod h1:KO7H1HLYh1qaljuVZJQwBFk1lRce6pJzt+C81GEnrlM= +go.etcd.io/etcd/api/v3 v3.7.1 h1:KJG0/DcWGfe3Y1otDf/fsBf0TSSgpxZ5RO/L8SFt73E= +go.etcd.io/etcd/api/v3 v3.7.1/go.mod h1:8bXIpCMeV7E3/XL0Ix123ATn3dB+0V7d9zklHbB0m78= +go.etcd.io/etcd/client/pkg/v3 v3.7.1 h1:rKYsj3pRkR0eK3yjT3XOgrhqfmIfj9pzNgxjh7mfFv4= +go.etcd.io/etcd/client/pkg/v3 v3.7.1/go.mod h1:cnzZGIUzSfjEwLC6UBVsSXlEK1eepS/JUD7wE6PLRT0= +go.etcd.io/etcd/client/v3 v3.7.1 h1:0PEMMC0KuZmVIN+RAbdqfkZ45pYTgKVtmBEbRCvZFUg= +go.etcd.io/etcd/client/v3 v3.7.1/go.mod h1:ffNqALa8tRCYhYo1F9oR489y23K39Gz+BSR3ApAGYq0= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= -go.opentelemetry.io/contrib/detectors/gcp v1.43.0 h1:62yY3dT7/ShwOxzA0RsKRgshBmfElKI4d/Myu2OxDFU= -go.opentelemetry.io/contrib/detectors/gcp v1.43.0/go.mod h1:RyaZMFY7yi1kAs45S6mbFGz8O8rqB0dTY14uzvG4LCs= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0 h1:yI1/OhfEPy7J9eoa6Sj051C7n5dvpj0QX8g4sRchg04= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0/go.mod h1:NoUCKYWK+3ecatC4HjkRktREheMeEtrXoQxrqYFeHSc= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0 h1:8tvICD4vSTOOsNrsI4Ljf6C+6UKvpTEH5XY3JMoyPoo= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0/go.mod h1:z9+yiacE0IHRqM4qFfkbt/JYlmYXgss8GY/jXoNuPJI= -go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU= -go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc= +go.opentelemetry.io/contrib/detectors/gcp v1.44.0 h1:NmLfL734pJhM0JKaYd2Y28+nY9dPRWYAAbxhRCrKXPw= +go.opentelemetry.io/contrib/detectors/gcp v1.44.0/go.mod h1:tNAsgd8avTGke1+MndXlU5Cru4PQ9Ai/cCNWQv/ZJ/s= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.70.0 h1:oECp5f+hN7nkwjU/8BxQ/q23bGPb8FIrD839owX222E= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.70.0/go.mod h1:DqEFwLumhzMBDQv9PcWbyoDxHI/4lAk6CM4nJBH39sc= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.70.0 h1:LMuyCAyfalSjDyjdC65nK6N0zoTT63+E/u95X0JovZI= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.70.0/go.mod h1:085m8qbm4hgc8rZWGDEa4vmyyo2c3nPxUslYUKUIU04= +go.opentelemetry.io/otel v1.45.0 h1:pdrWmLHofpubmArBv1LgFSv1Z0Ie/ppdZzu+kUN5EeU= +go.opentelemetry.io/otel v1.45.0/go.mod h1:XZxIqPapzEYnhNSScF5DIqXhm/rYi0FzCe2XddAwZfQ= go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.40.0 h1:ZrPRak/kS4xI3AVXy8F7pipuDXmDsrO8Lg+yQjBLjw0= go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.40.0/go.mod h1:3y6kQCWztq6hyW8Z9YxQDDm0Je9AJoFar2G0yDcmhRk= -go.opentelemetry.io/otel/metric v1.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc= -go.opentelemetry.io/otel/metric v1.44.0/go.mod h1:8O7hanEPBNgEMmybD3s2VBKcgWOCsA6tzHBPODAiquo= -go.opentelemetry.io/otel/metric/x v0.66.0 h1:YkCrx1zLOChi9ZcZ6euupOcsgzbVlec7D/xoEU1+cTA= -go.opentelemetry.io/otel/metric/x v0.66.0/go.mod h1:d1+BDj9t96do0/1LoU1ayfCv79ZgNE41qbhBvnMOBZk= -go.opentelemetry.io/otel/sdk v1.44.0 h1:nHYwb9lK+fJPU/dnT6s7W7Z8itMWyqrnVfbheVYrZ58= -go.opentelemetry.io/otel/sdk v1.44.0/go.mod h1:Osuydd3Se74nqjAKxid74N5eC+jfEqfTegHRnq58oK0= -go.opentelemetry.io/otel/sdk/metric v1.44.0 h1:3LlKgI+VjbVsjNRFZJZAJ30WjXC5VkNRks6si09iEfI= -go.opentelemetry.io/otel/sdk/metric v1.44.0/go.mod h1:5B5pMARnXxKhltooO4xUuCBorl65a4EpnTalObqOigA= -go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk= -go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE= +go.opentelemetry.io/otel/metric v1.45.0 h1:7Eg1uH7CJ5cXv9is6tnBe1FI6rj1nwUdbFypRm3br/M= +go.opentelemetry.io/otel/metric v1.45.0/go.mod h1:HAPbm1nd3p1PmFH7v2dR+6BjXxw+Lq4a2+pndMAm08s= +go.opentelemetry.io/otel/metric/x v0.67.0 h1:PcicCNZFkZ4bXfSooXdo3WN7RBOVOtjVdo1wD358Uns= +go.opentelemetry.io/otel/metric/x v0.67.0/go.mod h1:FBjCWZe6wgcqxcMtjdGiClDKXb2YxxXii0CXftE4QtI= +go.opentelemetry.io/otel/sdk v1.45.0 h1:4VVSMgQ83dUgW2aoX5f6JgLvHwIvzcuLnF9lUdCSpCw= +go.opentelemetry.io/otel/sdk v1.45.0/go.mod h1:Sr40LgXV7DsKMMJMKOhUWOgMWTfAaqvm2kF0g7ilwuA= +go.opentelemetry.io/otel/sdk/metric v1.45.0 h1:oVFszMfyj1Am6s24Vtc7wBb8BKLcwepJjNEYILuiE3o= +go.opentelemetry.io/otel/sdk/metric v1.45.0/go.mod h1:vUWUxDZvu1WVRj8JA8S0AdhsPrZoDpA2DdZauIh4mDA= +go.opentelemetry.io/otel/trace v1.45.0 h1:l/mP6Uv7oNO7/TblbhpbgMidxhq1uO/rPsikOyVhxag= +go.opentelemetry.io/otel/trace v1.45.0/go.mod h1:qoJJA2xNMnxRrdISU/kLtfUH2wNeQbiv+jhs/CxI8bc= go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= @@ -728,8 +726,8 @@ go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ= go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ= go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw= go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg= -goftp.io/server/v2 v2.0.2 h1:tkZpqyXys+vC15W5yGMi8Kzmbv1QSgeKr8qJXBnJbm8= -goftp.io/server/v2 v2.0.2/go.mod h1:Fl1WdcV7fx1pjOWx7jEHb7tsJ8VwE7+xHu6bVJ6r2qg= +goftp.io/server/v2 v2.0.3 h1:iz6Gxj7f2SFQVxrj0s1is+gueE6O9yTc+Ab0vtQ6Zn4= +goftp.io/server/v2 v2.0.3/go.mod h1:Fl1WdcV7fx1pjOWx7jEHb7tsJ8VwE7+xHu6bVJ6r2qg= golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= @@ -742,15 +740,15 @@ golang.org/x/crypto v0.0.0-20211209193657-4570a0811e8b/go.mod h1:IxCIyHEi3zRg3s0 golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU= golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= -golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw= -golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk= -golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa h1:Zt3DZoOFFYkKhDT3v7Lm9FDMEV06GpzjG2jrqW+QTE0= -golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa/go.mod h1:K79w1Vqn7PoiZn+TkNpx3BUWUQksGO3JcVX6qIjytmA= +golang.org/x/crypto v0.56.0 h1:GUh5Ii4J5jtcseSMiRqr1jXCNHoxjeV9Fmekc2oLy6Y= +golang.org/x/crypto v0.56.0/go.mod h1:OMW5y6CY9l38uPLmxU6l6pwcXp1obtLo3e6gT7gQR2I= +golang.org/x/exp v0.0.0-20260824195058-e88cd73687aa h1:QSyA8ishJCyT21kER9KwNt0b7BM3iRK4x9QXhjN5Fdk= +golang.org/x/exp v0.0.0-20260824195058-e88cd73687aa/go.mod h1:zeBbvyFKDaLwa7CH/zI8KXt7gTl14SF7sO08Pl5jBCM= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ= -golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0= +golang.org/x/mod v0.40.0 h1:hUv+3cXcdRHz08UmSiOob7sadHig73uo5bkXxQ/tvUs= +golang.org/x/mod v0.40.0/go.mod h1:0/weTWkPWGBikyTWAX3dkjVztMmBA5hM0DH6BElSupE= golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -764,8 +762,8 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE= -golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU= +golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To= +golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU= golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -822,8 +820,8 @@ golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs= -golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY= +golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8= +golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M= golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= @@ -833,36 +831,38 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q= -golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA= +golang.org/x/tools v0.49.0 h1:3NI7VXzL9+1WZD52Dx2ttoPwD5DWrFGpl9mFZDlmisI= +golang.org/x/tools v0.49.0/go.mod h1:SJNXV9DBKT0UbdttsQjbfJlAE/q+y36++zo3uL3N0Oo= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= -google.golang.org/api v0.278.0 h1:W7jiRvRi53VYFfZ/HoZjQBtJk7gOFbHD8ot1RzVZU6E= -google.golang.org/api v0.278.0/go.mod h1:B9TqLBwJqVjp1mtt7WeoQwWRwvu/400y5lETOql+giQ= +google.golang.org/api v0.290.0 h1:eMw0Xo+IfbbMlKmW7aHvpyQRv9RCXuWx/vs8AD+0x9A= +google.golang.org/api v0.290.0/go.mod h1:weJZ3lldHFYI0DBFNKpJelUDNnusTt5YaOEgxvt8ci8= google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7 h1:XzmzkmB14QhVhgnawEVsOn6OFsnpyxNPRY9QV01dNB0= google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7/go.mod h1:L43LFes82YgSonw6iTXTxXUX1OlULt4AQtkik4ULL/I= -google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa h1:Kjn0N0tCrDgiAFW+lGO4JZ3ck44CehvJQMAwj9QF0G8= -google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:q4lMZS6kskjT5HvCPrnnypcDPVJqT/f4nfxmkE7gryY= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa h1:mZHHdPZl0dbGHCflZgAq/Q468DWVFcU2whhB2KAo8fk= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= -google.golang.org/grpc v1.82.1 h1:NnAxzGRA0677vCa4BUkOAnO5+FfQqVl9iUXeD0IqcGE= -google.golang.org/grpc v1.82.1/go.mod h1:yzTZ1TB1Z3SG+LIYaI+WiE8D5+PZ3ArnrSp8zF3+/ZA= +google.golang.org/genproto/googleapis/api v0.0.0-20260831171406-18b4a7587f8a h1:i3TAXhpKc7TUP1VAPiBBrv45kamjoizCC3rOC0cAbOs= +google.golang.org/genproto/googleapis/api v0.0.0-20260831171406-18b4a7587f8a/go.mod h1:CvYJHpbzPlT0fb/PsgtAamdwru/GVxUsomFdXTpOTI8= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260831171406-18b4a7587f8a h1:3Dnd1cDaZlB68lziofO+bJXpjOy8UfRv8Unt+yH8tQ4= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260831171406-18b4a7587f8a/go.mod h1:DjtHYE8FKJLivXcBEjGwndXfIC23G0VpXiXKqG179uA= +google.golang.org/grpc v1.83.2 h1:EManeRomTObA0BU7I8vXgg/78uE5MJ9M8B39EX2WscU= +google.golang.org/grpc v1.83.2/go.mod h1:YPI1hK3kDked6iHvgX3tR0y+nX/qpMFKhPgFsokw1S8= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= -google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc= +google.golang.org/protobuf v1.36.12/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/ini.v1 v1.67.3 h1:iM9Lhz5MRSGhHVGGwCuzG9KO8PoirCXj/m/qTmOJJQw= +gopkg.in/ini.v1 v1.67.3/go.mod h1:x/cyOwCgZqOkJoDIJ3c1KNHMo10+nLGAhh+kn3Zizss= gopkg.in/urfave/cli.v1 v1.20.0/go.mod h1:vuBzUtMdQeixQj8LVd+/98pzhxNGQoyuPBlsXHOQNO0= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= diff --git a/helm/silo/Chart.yaml b/helm/silo/Chart.yaml index 9e07e965f..ba3123e00 100644 --- a/helm/silo/Chart.yaml +++ b/helm/silo/Chart.yaml @@ -1,8 +1,8 @@ apiVersion: v1 description: S3-Interface Libre Object Storage name: silo -version: 7.0.1 -appVersion: RELEASE.2026-08-06T00-00-00Z +version: 7.0.2 +appVersion: RELEASE.2026-09-03T13-18-01Z keywords: - silo - storage diff --git a/helm/silo/README.md b/helm/silo/README.md index 1f9ee8370..6b1d293e4 100644 --- a/helm/silo/README.md +++ b/helm/silo/README.md @@ -61,6 +61,7 @@ names. Export the complete values and render the candidate chart offline: ```bash helm get values my-release -n my-namespace -a > values.before-silo.yaml SILO_TAG='' +MCLI_TAG='' helm template my-release ./helm/silo \ -n my-namespace \ @@ -69,9 +70,9 @@ helm template my-release ./helm/silo \ --set fullnameOverride=my-existing-fullname \ --set serviceAccount.name=minio-sa \ --set image.repository=pgsty/silo \ - --set mcImage.repository=pgsty/silo \ + --set mcImage.repository=pgsty/mc \ --set-string image.tag="${SILO_TAG}" \ - --set-string mcImage.tag="${SILO_TAG}" \ + --set-string mcImage.tag="${MCLI_TAG}" \ > rendered.silo.yaml ``` @@ -81,6 +82,7 @@ mounts. After review, upgrade the chart and image together: ```bash SILO_TAG='' +MCLI_TAG='' helm upgrade my-release ./helm/silo \ -n my-namespace \ -f values.before-silo.yaml \ @@ -88,9 +90,9 @@ helm upgrade my-release ./helm/silo \ --set fullnameOverride=my-existing-fullname \ --set serviceAccount.name=minio-sa \ --set image.repository=pgsty/silo \ - --set mcImage.repository=pgsty/silo \ + --set mcImage.repository=pgsty/mc \ --set-string image.tag="${SILO_TAG}" \ - --set-string mcImage.tag="${SILO_TAG}" + --set-string mcImage.tag="${MCLI_TAG}" ``` Rollback is chart-level: use `helm rollback`, not an image-only downgrade. The diff --git a/helm/silo/values.yaml b/helm/silo/values.yaml index 646521430..3f702e7d2 100644 --- a/helm/silo/values.yaml +++ b/helm/silo/values.yaml @@ -15,18 +15,17 @@ clusterDomain: cluster.local ## image: repository: pgsty/silo - tag: RELEASE.2026-08-06T00-00-00Z + tag: RELEASE.2026-09-03T13-18-01Z pullPolicy: IfNotPresent imagePullSecrets: [] # - name: "image-pull-secret" -## Image used by post-install jobs. The pgsty/silo image bundles mcli and -## provides /usr/bin/mc as a compatibility link. +## Silo client image used by post-install jobs. ## mcImage: - repository: pgsty/silo - tag: RELEASE.2026-08-06T00-00-00Z + repository: pgsty/mc + tag: RELEASE.2026-09-03T07-13-05Z pullPolicy: IfNotPresent ## Silo mode, i.e. standalone or distributed. diff --git a/internal/bucket/cors/cors.go b/internal/bucket/cors/cors.go new file mode 100644 index 000000000..7d5eb0c4c --- /dev/null +++ b/internal/bucket/cors/cors.go @@ -0,0 +1,364 @@ +// Copyright (c) 2015-2021 MinIO, Inc. +// +// This file is part of MinIO Object Storage stack +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +// Package cors implements the S3 per-bucket CORS configuration type, +// its validation, and origin/method/header matching helpers. +package cors + +import ( + "encoding/xml" + "errors" + "fmt" + "io" + "strconv" + "strings" + "unicode/utf8" +) + +// maxCORSRules is the maximum number of rules allowed per bucket (AWS S3 limit). +const maxCORSRules = 100 + +// maxCORSRuleIDLen is the maximum length of a CORSRule (AWS S3 limit). +const maxCORSRuleIDLen = 255 + +// maxCORSMaxAgeSeconds is the largest value representable by the int32 +// MaxAgeSeconds shape used by the S3 API model. +const maxCORSMaxAgeSeconds = 1<<31 - 1 + +// supportedMethods are the HTTP methods permitted in an AllowedMethod element. +var supportedMethods = map[string]bool{ + "GET": true, + "PUT": true, + "HEAD": true, + "POST": true, + "DELETE": true, +} + +// Config is the S3 document. +type Config struct { + XMLName xml.Name `xml:"CORSConfiguration"` + CORSRules []Rule `xml:"CORSRule"` +} + +// Rule is a single . +type Rule struct { + ID string `xml:"ID,omitempty"` + AllowedHeaders []string `xml:"AllowedHeader"` + AllowedMethods []string `xml:"AllowedMethod"` + AllowedOrigins []string `xml:"AllowedOrigin"` + ExposeHeaders []string `xml:"ExposeHeader"` + MaxAgeSeconds int `xml:"MaxAgeSeconds"` + + maxAgeSecondsSet bool +} + +type corsXMLUnknown struct { + XMLName xml.Name +} + +type corsXMLValue struct { + Text string `xml:",chardata"` + Unknown []corsXMLUnknown `xml:",any"` +} + +type configXML struct { + XMLName xml.Name `xml:"CORSConfiguration"` + CORSRules []ruleXML `xml:"CORSRule"` + Text string `xml:",chardata"` + Unknown []corsXMLUnknown `xml:",any"` +} + +type ruleXML struct { + ID []corsXMLValue `xml:"ID"` + AllowedHeaders []corsXMLValue `xml:"AllowedHeader"` + AllowedMethods []corsXMLValue `xml:"AllowedMethod"` + AllowedOrigins []corsXMLValue `xml:"AllowedOrigin"` + ExposeHeaders []corsXMLValue `xml:"ExposeHeader"` + MaxAgeSeconds []corsXMLValue `xml:"MaxAgeSeconds"` + Text string `xml:",chardata"` + Unknown []corsXMLUnknown `xml:",any"` +} + +// ParseBucketCorsConfig parses a CORS configuration from the given reader. +func ParseBucketCorsConfig(r io.Reader) (*Config, error) { + var parsed configXML + decoder := xml.NewDecoder(r) + if err := decoder.Decode(&parsed); err != nil { + return nil, err + } + if strings.TrimSpace(parsed.Text) != "" { + return nil, xml.UnmarshalError("unexpected character data in CORSConfiguration") + } + if len(parsed.Unknown) > 0 { + return nil, xml.UnmarshalError(fmt.Sprintf("unexpected element <%s> in CORSConfiguration", parsed.Unknown[0].XMLName.Local)) + } + + c := Config{ + XMLName: parsed.XMLName, + CORSRules: make([]Rule, len(parsed.CORSRules)), + } + for i := range parsed.CORSRules { + rule, err := parseCORSRuleXML(parsed.CORSRules[i]) + if err != nil { + return nil, err + } + c.CORSRules[i] = rule + } + + // Decode consumes one document element. Only XML whitespace, comments, and + // processing instructions are permitted after it. + for { + token, err := decoder.Token() + if err == io.EOF { + break + } + if err != nil { + return nil, err + } + switch token := token.(type) { + case xml.CharData: + if strings.TrimSpace(string(token)) == "" { + continue + } + case xml.Comment, xml.ProcInst: + continue + } + return nil, errors.New("unexpected XML content after CORSConfiguration") + } + return &c, nil +} + +func parseCORSRuleXML(parsed ruleXML) (Rule, error) { + if strings.TrimSpace(parsed.Text) != "" { + return Rule{}, xml.UnmarshalError("unexpected character data in CORSRule") + } + if len(parsed.Unknown) > 0 { + return Rule{}, xml.UnmarshalError(fmt.Sprintf("unexpected element <%s> in CORSRule", parsed.Unknown[0].XMLName.Local)) + } + if len(parsed.ID) > 1 { + return Rule{}, xml.UnmarshalError("duplicate ID element in CORSRule") + } + if len(parsed.MaxAgeSeconds) > 1 { + return Rule{}, xml.UnmarshalError("duplicate MaxAgeSeconds element in CORSRule") + } + + rule := Rule{} + var err error + if len(parsed.ID) == 1 { + if rule.ID, err = corsXMLText("ID", parsed.ID[0]); err != nil { + return Rule{}, err + } + } + if rule.AllowedHeaders, err = corsXMLTexts("AllowedHeader", parsed.AllowedHeaders); err != nil { + return Rule{}, err + } + if rule.AllowedMethods, err = corsXMLTexts("AllowedMethod", parsed.AllowedMethods); err != nil { + return Rule{}, err + } + if rule.AllowedOrigins, err = corsXMLTexts("AllowedOrigin", parsed.AllowedOrigins); err != nil { + return Rule{}, err + } + if rule.ExposeHeaders, err = corsXMLTexts("ExposeHeader", parsed.ExposeHeaders); err != nil { + return Rule{}, err + } + if len(parsed.MaxAgeSeconds) == 1 { + value, valueErr := corsXMLText("MaxAgeSeconds", parsed.MaxAgeSeconds[0]) + if valueErr != nil { + return Rule{}, valueErr + } + age, parseErr := strconv.ParseInt(strings.TrimSpace(value), 10, 32) + if parseErr != nil { + return Rule{}, xml.UnmarshalError("invalid MaxAgeSeconds value") + } + rule.MaxAgeSeconds = int(age) + rule.maxAgeSecondsSet = true + } + return rule, nil +} + +func corsXMLTexts(name string, values []corsXMLValue) ([]string, error) { + result := make([]string, len(values)) + for i := range values { + value, err := corsXMLText(name, values[i]) + if err != nil { + return nil, err + } + result[i] = value + } + return result, nil +} + +func corsXMLText(name string, value corsXMLValue) (string, error) { + if len(value.Unknown) > 0 { + return "", xml.UnmarshalError(fmt.Sprintf("element <%s> must not contain child element <%s>", name, value.Unknown[0].XMLName.Local)) + } + return value.Text, nil +} + +// Validate checks the config against the S3 constraints. +func (c *Config) Validate() error { + if len(c.CORSRules) == 0 { + return errors.New("CORSConfiguration must contain at least one rule") + } + if len(c.CORSRules) > maxCORSRules { + return errors.New("CORSConfiguration exceeds the maximum number of rules") + } + for _, r := range c.CORSRules { + if !utf8.ValidString(r.ID) { + return errors.New("CORSRule ID must contain valid UTF-8") + } + if utf8.RuneCountInString(r.ID) > maxCORSRuleIDLen { + return errors.New("CORSRule ID exceeds the maximum length of 255 characters") + } + if len(r.AllowedOrigins) == 0 { + return errors.New("CORSRule must contain at least one AllowedOrigin") + } + if len(r.AllowedMethods) == 0 { + return errors.New("CORSRule must contain at least one AllowedMethod") + } + for _, o := range r.AllowedOrigins { + if o == "" { + return errors.New("AllowedOrigin must not be empty") + } + if strings.Contains(o, "?") { + return errors.New("AllowedOrigin may not contain wildcard '?': " + o) + } + if strings.Count(o, "*") > 1 { + return errors.New("AllowedOrigin may contain at most one wildcard '*': " + o) + } + } + for _, m := range r.AllowedMethods { + if !supportedMethods[m] { + return errors.New("unsupported method in CORSRule: " + m) + } + } + for _, h := range r.AllowedHeaders { + if h == "" { + return errors.New("AllowedHeader must not be empty") + } + if strings.Contains(h, "?") { + return errors.New("AllowedHeader may not contain wildcard '?': " + h) + } + if strings.Count(h, "*") > 1 { + return errors.New("AllowedHeader may contain at most one wildcard '*': " + h) + } + } + for _, h := range r.ExposeHeaders { + if h == "" { + return errors.New("ExposeHeader must not be empty") + } + } + if r.MaxAgeSeconds < 0 { + return errors.New("MaxAgeSeconds must not be negative") + } + if int64(r.MaxAgeSeconds) > maxCORSMaxAgeSeconds { + return errors.New("MaxAgeSeconds exceeds the maximum S3 integer value") + } + } + return nil +} + +func matchSingleWildcard(pattern, value string) bool { + prefix, suffix, found := strings.Cut(pattern, "*") + if !found { + return pattern == value + } + return len(value) >= len(prefix)+len(suffix) && + strings.HasPrefix(value, prefix) && strings.HasSuffix(value, suffix) +} + +func (r Rule) matchAllowedOrigin(origin string) (string, bool) { + for _, allowedOrigin := range r.AllowedOrigins { + if matchSingleWildcard(allowedOrigin, origin) { + return allowedOrigin, true + } + } + return "", false +} + +// HasAllowedMethod reports whether the rule allows the given HTTP method. +func (r Rule) HasAllowedMethod(method string) bool { + for _, m := range r.AllowedMethods { + if m == method { + return true + } + } + return false +} + +// FilterAllowedHeaders returns the subset of reqHeaders permitted by the rule +// and whether every requested header was allowed. +func (r Rule) FilterAllowedHeaders(reqHeaders []string) ([]string, bool) { + var allowed []string + for _, h := range reqHeaders { + h = strings.TrimSpace(h) + if h == "" { + continue + } + if !r.headerAllowed(h) { + return nil, false + } + allowed = append(allowed, h) + } + return allowed, true +} + +func (r Rule) headerAllowed(header string) bool { + for _, h := range r.AllowedHeaders { + if matchSingleWildcard(strings.ToLower(h), strings.ToLower(header)) { + return true + } + } + return false +} + +// MatchRule returns the first rule whose origin and method both match, along +// with the configured origin pattern that matched. +func (c *Config) MatchRule(origin, method string) (rule *Rule, allowedOrigin string, ok bool) { + for i := range c.CORSRules { + r := &c.CORSRules[i] + matchedOrigin, originOK := r.matchAllowedOrigin(origin) + if originOK && r.HasAllowedMethod(method) { + return r, matchedOrigin, true + } + } + return nil, "", false +} + +// MatchPreflight returns the first rule whose origin and method match and +// whose AllowedHeaders permit every header in reqHeaders. Unlike MatchRule, +// this keeps evaluating subsequent rules until one fully satisfies the +// preflight request, since an earlier origin/method match with a more +// restrictive header list must not shadow a later, more permissive rule. +func (c *Config) MatchPreflight(origin, method string, reqHeaders []string) (rule *Rule, allowedOrigin string, allowedHeaders []string, maxAgeSeconds *int, ok bool) { + for i := range c.CORSRules { + r := &c.CORSRules[i] + matchedOrigin, originOK := r.matchAllowedOrigin(origin) + if !originOK || !r.HasAllowedMethod(method) { + continue + } + allowed, headersOK := r.FilterAllowedHeaders(reqHeaders) + if !headersOK { + continue + } + if r.maxAgeSecondsSet || r.MaxAgeSeconds != 0 { + maxAgeSeconds = &r.MaxAgeSeconds + } + return r, matchedOrigin, allowed, maxAgeSeconds, true + } + return nil, "", nil, nil, false +} diff --git a/internal/bucket/cors/cors_adversarial_test.go b/internal/bucket/cors/cors_adversarial_test.go new file mode 100644 index 000000000..794c298fe --- /dev/null +++ b/internal/bucket/cors/cors_adversarial_test.go @@ -0,0 +1,222 @@ +// Copyright (c) 2015-2021 MinIO, Inc. +// +// This file is part of MinIO Object Storage stack +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +package cors + +import ( + "strconv" + "strings" + "testing" +) + +func TestParseStandardS3Namespace(t *testing.T) { + doc := `https://app.example.comGET` + cfg, err := ParseBucketCorsConfig(strings.NewReader(doc)) + if err != nil { + t.Fatal(err) + } + if err = cfg.Validate(); err != nil { + t.Fatal(err) + } + if _, _, ok := cfg.MatchRule("https://app.example.com", "GET"); !ok { + t.Fatal("standard S3 namespace document did not produce a matching rule") + } +} + +const minimalCORSConfig = `*GET` + +func TestParseRejectsTrailingXMLRoot(t *testing.T) { + for name, suffix := range map[string]string{ + "second root": ``, + "text": `junk`, + "dangling close": ``, + } { + t.Run(name, func(t *testing.T) { + if _, err := ParseBucketCorsConfig(strings.NewReader(minimalCORSConfig + suffix)); err == nil { + t.Fatalf("expected trailing %s to be rejected", name) + } + }) + } +} + +func TestParseAllowsXMLMiscAfterRoot(t *testing.T) { + for name, suffix := range map[string]string{ + "whitespace": " \n\t", + "comment": ``, + "processing instruction": ``, + } { + t.Run(name, func(t *testing.T) { + if _, err := ParseBucketCorsConfig(strings.NewReader(minimalCORSConfig + suffix)); err != nil { + t.Fatalf("valid trailing XML misc was rejected: %v", err) + } + }) + } +} + +func TestValidateCORSRuleIDCountsCharacters(t *testing.T) { + doc := `` + strings.Repeat("界", 255) + `*GET` + cfg, err := ParseBucketCorsConfig(strings.NewReader(doc)) + if err != nil { + t.Fatalf("parse failed: %v", err) + } + if err = cfg.Validate(); err != nil { + t.Fatalf("255-character rule ID must be accepted: %v", err) + } + + cfg.CORSRules[0].ID += "界" + if err = cfg.Validate(); err == nil { + t.Fatal("256-character rule ID must be rejected") + } +} + +func TestValidateRejectsNonCanonicalAllowedMethod(t *testing.T) { + doc := `*get` + cfg, err := ParseBucketCorsConfig(strings.NewReader(doc)) + if err != nil { + t.Fatalf("parse failed: %v", err) + } + if err = cfg.Validate(); err == nil { + t.Fatal("expected lowercase AllowedMethod to be rejected") + } +} + +func TestAllowedMethodMatchingIsCaseSensitive(t *testing.T) { + rule := Rule{AllowedMethods: []string{"GET"}} + if !rule.HasAllowedMethod("GET") { + t.Fatal("expected canonical GET to match") + } + if rule.HasAllowedMethod("get") { + t.Fatal("lowercase request method must not match canonical GET") + } +} + +func TestParseRejectsElementsOutsideCORSShape(t *testing.T) { + tests := map[string]string{ + "unknown root child": `*GET`, + "unknown rule child": `*GET`, + "nested origin child": `GET`, + "duplicate id": `ab*GET`, + "duplicate max age": `*GET12`, + "empty max age": `*GET`, + "overflow max age": `*GET2147483648`, + } + + for name, doc := range tests { + t.Run(name, func(t *testing.T) { + if _, err := ParseBucketCorsConfig(strings.NewReader(doc)); err == nil { + t.Fatal("expected parse error") + } + }) + } +} + +func TestMaxAgeSecondsPresence(t *testing.T) { + tests := []struct { + name string + element string + value int + present bool + }{ + {name: "absent"}, + {name: "zero", element: `0`, present: true}, + {name: "positive", element: `3000`, value: 3000, present: true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + doc := `*GET` + tt.element + `` + cfg, err := ParseBucketCorsConfig(strings.NewReader(doc)) + if err != nil { + t.Fatalf("parse failed: %v", err) + } + rule := cfg.CORSRules[0] + _, _, _, maxAgeSeconds, ok := cfg.MatchPreflight("https://example.com", "GET", nil) + if !ok { + t.Fatal("expected rule to match") + } + present := maxAgeSeconds != nil + if rule.MaxAgeSeconds != tt.value || present != tt.present { + t.Fatalf("MaxAgeSeconds = %d, present = %v", rule.MaxAgeSeconds, present) + } + }) + } +} + +func TestValidateRuleCountBoundary(t *testing.T) { + rule := Rule{AllowedOrigins: []string{"*"}, AllowedMethods: []string{"GET"}} + cfg := Config{CORSRules: make([]Rule, 100)} + for i := range cfg.CORSRules { + cfg.CORSRules[i] = rule + } + if err := cfg.Validate(); err != nil { + t.Fatalf("100 rules must be accepted: %v", err) + } + cfg.CORSRules = append(cfg.CORSRules, rule) + if err := cfg.Validate(); err == nil { + t.Fatal("101 rules must be rejected") + } +} + +func TestValidateMaxAgeSecondsBoundary(t *testing.T) { + cfg := Config{CORSRules: []Rule{{ + AllowedOrigins: []string{"*"}, + AllowedMethods: []string{"GET"}, + MaxAgeSeconds: maxCORSMaxAgeSeconds, + }}} + if err := cfg.Validate(); err != nil { + t.Fatalf("MaxAgeSeconds int32 maximum must be accepted: %v", err) + } + if strconv.IntSize > 32 { + overflow := int64(maxCORSMaxAgeSeconds) + 1 + cfg.CORSRules[0].MaxAgeSeconds = int(overflow) + if err := cfg.Validate(); err == nil { + t.Fatal("MaxAgeSeconds above int32 maximum must be rejected") + } + } +} + +func TestSingleWildcardMatching(t *testing.T) { + tests := []struct { + pattern string + value string + want bool + }{ + {"*", "https://example.com", true}, + {"https://*.example.com", "https://api.example.com", true}, + {"https://*.example.com", "https://.example.com", true}, + {"https://*.example.com", "http://api.example.com", false}, + {"https://?.example.com", "https://a.example.com", false}, + } + for _, tt := range tests { + if got := matchSingleWildcard(tt.pattern, tt.value); got != tt.want { + t.Errorf("matchSingleWildcard(%q, %q) = %v, want %v", tt.pattern, tt.value, got, tt.want) + } + } +} + +func TestMatchRuleReturnsMatchedOriginPattern(t *testing.T) { + cfg := Config{CORSRules: []Rule{{ + AllowedOrigins: []string{"https://app.example.com", "https://*", "*"}, + AllowedMethods: []string{"GET"}, + }}} + tests := []struct { + origin string + want string + }{ + {"https://app.example.com", "https://app.example.com"}, + {"https://other.example.com", "https://*"}, + {"http://other.example.com", "*"}, + } + for _, tt := range tests { + _, got, ok := cfg.MatchRule(tt.origin, "GET") + if !ok || got != tt.want { + t.Errorf("origin %q matched %q, ok=%v; want %q", tt.origin, got, ok, tt.want) + } + } +} diff --git a/internal/bucket/cors/cors_test.go b/internal/bucket/cors/cors_test.go new file mode 100644 index 000000000..dbf56f5b1 --- /dev/null +++ b/internal/bucket/cors/cors_test.go @@ -0,0 +1,170 @@ +// Copyright (c) 2015-2021 MinIO, Inc. +// +// This file is part of MinIO Object Storage stack +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package cors + +import ( + "strings" + "testing" +) + +const sampleCORS = ` + + rule1 + http://www.example.com + https://*.example.org + GET + PUT + x-amz-* + ETag + 3000 + +` + +func TestParseAndValidate(t *testing.T) { + c, err := ParseBucketCorsConfig(strings.NewReader(sampleCORS)) + if err != nil { + t.Fatalf("parse failed: %v", err) + } + if err := c.Validate(); err != nil { + t.Fatalf("validate failed: %v", err) + } + if len(c.CORSRules) != 1 { + t.Fatalf("expected 1 rule, got %d", len(c.CORSRules)) + } + if c.CORSRules[0].MaxAgeSeconds != 3000 { + t.Fatalf("MaxAgeSeconds mismatch: %d", c.CORSRules[0].MaxAgeSeconds) + } +} + +func TestValidateRejections(t *testing.T) { + cases := map[string]string{ + "bad method": `*TRACE`, + "no origin": `GET`, + "empty origin": `GET`, + "no method": `*`, + "negative age": `*GET-1`, + "multi wildcard origin": `https://*.*.example.comGET`, + "multi wildcard header": `*GETx-*-*`, + "question mark origin": `https://?.example.comGET`, + "question mark header": `*GETx-amz-?`, + "empty allowed header": `*GET`, + "empty expose header": `*GET`, + "overlong id": `` + strings.Repeat("a", 256) + `*GET`, + } + for name, doc := range cases { + c, err := ParseBucketCorsConfig(strings.NewReader(doc)) + if err != nil { + continue // parse-level rejection is acceptable + } + if err := c.Validate(); err == nil { + t.Errorf("%s: expected validation error, got nil", name) + } + } +} + +func TestMatching(t *testing.T) { + c, _ := ParseBucketCorsConfig(strings.NewReader(sampleCORS)) + rule, _, ok := c.MatchRule("https://api.example.org", "GET") + if !ok { + t.Fatal("expected origin+method to match") + } + if _, _, ok := c.MatchRule("http://evil.com", "GET"); ok { + t.Fatal("did not expect match for disallowed origin") + } + if _, _, ok := c.MatchRule("http://www.example.com", "DELETE"); ok { + t.Fatal("did not expect match for disallowed method") + } + allowed, ok := rule.FilterAllowedHeaders([]string{"x-amz-date", "x-amz-content-sha256"}) + if !ok || len(allowed) != 2 { + t.Fatalf("expected both headers allowed via wildcard, got %v ok=%v", allowed, ok) + } + if _, ok := rule.FilterAllowedHeaders([]string{"authorization"}); ok { + t.Fatal("did not expect authorization to be allowed") + } +} + +func TestMatchPreflightFallsThroughToLaterRule(t *testing.T) { + // Rule A matches origin+method but only allows a restrictive header set. + // Rule B, listed after A, matches the same origin+method and allows any + // header. A preflight requesting a header only B permits must not be + // rejected just because A was tried first. + const doc = ` + + A-restrictive + https://app.example.com + GET + x-amz-date + + + B-permissive + https://app.example.com + GET + * + +` + + c, err := ParseBucketCorsConfig(strings.NewReader(doc)) + if err != nil { + t.Fatalf("parse failed: %v", err) + } + + rule, _, allowed, _, ok := c.MatchPreflight("https://app.example.com", "GET", []string{"x-custom-header"}) + if !ok { + t.Fatal("expected MatchPreflight to succeed via the later, permissive rule") + } + if rule.ID != "B-permissive" { + t.Fatalf("expected rule B-permissive to be selected, got %q", rule.ID) + } + if len(allowed) != 1 || allowed[0] != "x-custom-header" { + t.Fatalf("unexpected allowed headers: %v", allowed) + } +} + +func TestMatchAllowedOriginReturnsFirstMatchingPattern(t *testing.T) { + rule := Rule{AllowedOrigins: []string{"https://app.example.com", "https://*", "*"}} + + tests := []struct { + origin string + want string + }{ + {"https://app.example.com", "https://app.example.com"}, + {"https://other.example.com", "https://*"}, + {"http://other.example.com", "*"}, + } + + for _, tt := range tests { + got, ok := rule.matchAllowedOrigin(tt.origin) + if !ok { + t.Fatalf("expected %q to match", tt.origin) + } + if got != tt.want { + t.Fatalf("origin %q matched %q, want %q", tt.origin, got, tt.want) + } + } +} + +func TestFilterAllowedHeadersPreservesRequestedNames(t *testing.T) { + rule := Rule{AllowedHeaders: []string{"x-amz-*"}} + allowed, ok := rule.FilterAllowedHeaders([]string{"X-Amz-Date", " X-AMZ-Meta-Test "}) + if !ok { + t.Fatal("expected both request headers to match") + } + if got := strings.Join(allowed, ","); got != "X-Amz-Date,X-AMZ-Meta-Test" { + t.Fatalf("allowed headers = %q", got) + } +} diff --git a/internal/bucket/object/lock/lock.go b/internal/bucket/object/lock/lock.go index 410011b96..b7bfdb9ef 100644 --- a/internal/bucket/object/lock/lock.go +++ b/internal/bucket/object/lock/lock.go @@ -26,16 +26,14 @@ import ( "io" "maps" "net/http" - "net/textproto" "strings" "time" "github.com/beevik/ntp" "github.com/minio/minio/internal/amztime" - xhttp "github.com/minio/minio/internal/http" "github.com/minio/minio/internal/logger" - "github.com/minio/pkg/v3/env" + "github.com/pgsty/silo-pkg/v3/env" ) const ( @@ -435,7 +433,7 @@ func IsObjectLockRequested(h http.Header) bool { } // ParseObjectLockRetentionHeaders parses http headers to extract retention mode and retention date -func ParseObjectLockRetentionHeaders(h http.Header) (rmode RetMode, r RetentionDate, err error) { +func ParseObjectLockRetentionHeaders(h http.Header, allowPastRetainDate bool) (rmode RetMode, r RetentionDate, err error) { retMode := h.Get(AmzObjectLockMode) dateStr := h.Get(AmzObjectLockRetainUntilDate) if len(retMode) == 0 || len(dateStr) == 0 { @@ -455,15 +453,13 @@ func ParseObjectLockRetentionHeaders(h http.Header) (rmode RetMode, r RetentionD if err != nil { return rmode, r, ErrInvalidRetentionDate } - _, replReq := h[textproto.CanonicalMIMEHeaderKey(xhttp.MinIOSourceReplicationRequest)] - t, err := UTCNowNTP() if err != nil { lockLogIf(context.Background(), err) return rmode, r, ErrPastObjectLockRetainDate } - if retDate.Before(t) && !replReq { + if retDate.Before(t) && !allowPastRetainDate { return rmode, r, ErrPastObjectLockRetainDate } diff --git a/internal/bucket/object/lock/lock_test.go b/internal/bucket/object/lock/lock_test.go index be7975e28..c53d77b86 100644 --- a/internal/bucket/object/lock/lock_test.go +++ b/internal/bucket/object/lock/lock_test.go @@ -386,7 +386,7 @@ func TestParseObjectLockRetentionHeaders(t *testing.T) { } for i, tt := range tests { - _, _, err := ParseObjectLockRetentionHeaders(tt.header) + _, _, err := ParseObjectLockRetentionHeaders(tt.header, false) //nolint:gocritic if tt.expectedErr == nil { if err != nil { @@ -398,6 +398,14 @@ func TestParseObjectLockRetentionHeaders(t *testing.T) { t.Fatalf("Case %d error: expected = %v, got = %v", i, tt.expectedErr, err) } } + + past := http.Header{ + xhttp.AmzObjectLockMode: []string{"governance"}, + xhttp.AmzObjectLockRetainUntilDate: []string{"2017-01-02T15:04:05Z"}, + } + if _, _, err := ParseObjectLockRetentionHeaders(past, true); err != nil { + t.Fatalf("trusted replica past retention date: %v", err) + } } func TestGetObjectRetentionMeta(t *testing.T) { diff --git a/internal/bucket/replication/destination.go b/internal/bucket/replication/destination.go index 9f31b3231..802813070 100644 --- a/internal/bucket/replication/destination.go +++ b/internal/bucket/replication/destination.go @@ -22,7 +22,7 @@ import ( "fmt" "strings" - "github.com/minio/pkg/v3/wildcard" + "github.com/pgsty/silo-pkg/v3/wildcard" ) // DestinationARNPrefix - destination ARN prefix as per AWS S3 specification. diff --git a/internal/bucket/versioning/versioning.go b/internal/bucket/versioning/versioning.go index 3647f908d..1e26ba5fe 100644 --- a/internal/bucket/versioning/versioning.go +++ b/internal/bucket/versioning/versioning.go @@ -22,7 +22,7 @@ import ( "io" "strings" - "github.com/minio/pkg/v3/wildcard" + "github.com/pgsty/silo-pkg/v3/wildcard" ) // State - enabled/disabled/suspended states diff --git a/internal/config/api/api.go b/internal/config/api/api.go index f203f7e95..e6b6a1eb5 100644 --- a/internal/config/api/api.go +++ b/internal/config/api/api.go @@ -28,7 +28,7 @@ import ( "time" "github.com/minio/minio/internal/config" - "github.com/minio/pkg/v3/env" + "github.com/pgsty/silo-pkg/v3/env" ) // API sub-system constants diff --git a/internal/config/batch/batch.go b/internal/config/batch/batch.go index 7404cf869..5b7f6e32d 100644 --- a/internal/config/batch/batch.go +++ b/internal/config/batch/batch.go @@ -22,7 +22,7 @@ import ( "time" "github.com/minio/minio/internal/config" - "github.com/minio/pkg/v3/env" + "github.com/pgsty/silo-pkg/v3/env" ) // Batch job environment variables diff --git a/internal/config/browser/browser.go b/internal/config/browser/browser.go index f5ad11d20..a4fe521eb 100644 --- a/internal/config/browser/browser.go +++ b/internal/config/browser/browser.go @@ -23,7 +23,7 @@ import ( "sync" "github.com/minio/minio/internal/config" - "github.com/minio/pkg/v3/env" + "github.com/pgsty/silo-pkg/v3/env" ) // Browser sub-system constants diff --git a/internal/config/callhome/callhome.go b/internal/config/callhome/callhome.go index ef6f8d51f..63680eac4 100644 --- a/internal/config/callhome/callhome.go +++ b/internal/config/callhome/callhome.go @@ -22,7 +22,7 @@ import ( "time" "github.com/minio/minio/internal/config" - "github.com/minio/pkg/v3/env" + "github.com/pgsty/silo-pkg/v3/env" ) // Callhome related keys diff --git a/internal/config/certs.go b/internal/config/certs.go index e2ba44ebc..c95807cdd 100644 --- a/internal/config/certs.go +++ b/internal/config/certs.go @@ -25,7 +25,7 @@ import ( "errors" "os" - "github.com/minio/pkg/v3/env" + "github.com/pgsty/silo-pkg/v3/env" ) // EnvCertPassword is the environment variable which contains the password used diff --git a/internal/config/compress/compress.go b/internal/config/compress/compress.go index dc050e3ba..5b122fb57 100644 --- a/internal/config/compress/compress.go +++ b/internal/config/compress/compress.go @@ -22,7 +22,7 @@ import ( "strings" "github.com/minio/minio/internal/config" - "github.com/minio/pkg/v3/env" + "github.com/pgsty/silo-pkg/v3/env" ) // Config represents the compression settings. diff --git a/internal/config/config.go b/internal/config/config.go index c254d50d1..7d7b1db5e 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -31,7 +31,7 @@ import ( "github.com/minio/madmin-go/v3" "github.com/minio/minio-go/v7/pkg/set" "github.com/minio/minio/internal/auth" - "github.com/minio/pkg/v3/env" + "github.com/pgsty/silo-pkg/v3/env" ) // ErrorConfig holds the config error types diff --git a/internal/config/drive/drive.go b/internal/config/drive/drive.go index 862c62ab7..d4e0d61de 100644 --- a/internal/config/drive/drive.go +++ b/internal/config/drive/drive.go @@ -22,7 +22,7 @@ import ( "time" "github.com/minio/minio/internal/config" - "github.com/minio/pkg/v3/env" + "github.com/pgsty/silo-pkg/v3/env" ) // Drive specific timeout environment variables diff --git a/internal/config/etcd/etcd.go b/internal/config/etcd/etcd.go index 87e18012b..9af396b2c 100644 --- a/internal/config/etcd/etcd.go +++ b/internal/config/etcd/etcd.go @@ -25,8 +25,8 @@ import ( "github.com/minio/minio/internal/config" "github.com/minio/minio/internal/crypto" - "github.com/minio/pkg/v3/env" - xnet "github.com/minio/pkg/v3/net" + "github.com/pgsty/silo-pkg/v3/env" + xnet "github.com/pgsty/silo-pkg/v3/net" clientv3 "go.etcd.io/etcd/client/v3" "go.etcd.io/etcd/client/v3/namespace" "go.uber.org/zap" diff --git a/internal/config/heal/heal.go b/internal/config/heal/heal.go index edca0eec3..a9f5e0c87 100644 --- a/internal/config/heal/heal.go +++ b/internal/config/heal/heal.go @@ -26,7 +26,7 @@ import ( "time" "github.com/minio/minio/internal/config" - "github.com/minio/pkg/v3/env" + "github.com/pgsty/silo-pkg/v3/env" ) // Compression environment variables diff --git a/internal/config/identity/ldap/config.go b/internal/config/identity/ldap/config.go index a3fc46d3e..156a99ffb 100644 --- a/internal/config/identity/ldap/config.go +++ b/internal/config/identity/ldap/config.go @@ -28,7 +28,7 @@ import ( "github.com/minio/madmin-go/v3" "github.com/minio/minio/internal/config" "github.com/minio/minio/internal/crypto" - "github.com/minio/pkg/v3/ldap" + "github.com/pgsty/silo-pkg/v3/ldap" ) const ( diff --git a/internal/config/identity/ldap/ldap.go b/internal/config/identity/ldap/ldap.go index 967b6d78c..05a2fccd0 100644 --- a/internal/config/identity/ldap/ldap.go +++ b/internal/config/identity/ldap/ldap.go @@ -27,7 +27,7 @@ import ( ldap "github.com/go-ldap/ldap/v3" "github.com/minio/minio-go/v7/pkg/set" "github.com/minio/minio/internal/auth" - xldap "github.com/minio/pkg/v3/ldap" + xldap "github.com/pgsty/silo-pkg/v3/ldap" ) var errAuthentication = errors.New("ldap authentication failed") diff --git a/internal/config/identity/openid/jwt.go b/internal/config/identity/openid/jwt.go index 48fe0ef72..506eedc00 100644 --- a/internal/config/identity/openid/jwt.go +++ b/internal/config/identity/openid/jwt.go @@ -31,8 +31,8 @@ import ( jwtgo "github.com/golang-jwt/jwt/v4" "github.com/minio/minio/internal/arn" "github.com/minio/minio/internal/auth" - xnet "github.com/minio/pkg/v3/net" - "github.com/minio/pkg/v3/policy" + xnet "github.com/pgsty/silo-pkg/v3/net" + "github.com/pgsty/silo-pkg/v3/policy" ) type publicKeys struct { diff --git a/internal/config/identity/openid/jwt_test.go b/internal/config/identity/openid/jwt_test.go index 77ae4fcfa..fc3379e86 100644 --- a/internal/config/identity/openid/jwt_test.go +++ b/internal/config/identity/openid/jwt_test.go @@ -39,7 +39,7 @@ import ( "github.com/minio/minio/internal/arn" "github.com/minio/minio/internal/config" jwtm "github.com/minio/minio/internal/jwt" - xnet "github.com/minio/pkg/v3/net" + xnet "github.com/pgsty/silo-pkg/v3/net" ) func TestUpdateClaimsExpiry(t *testing.T) { diff --git a/internal/config/identity/openid/openid.go b/internal/config/identity/openid/openid.go index bb620c1b8..0181253b2 100644 --- a/internal/config/identity/openid/openid.go +++ b/internal/config/identity/openid/openid.go @@ -39,9 +39,9 @@ import ( "github.com/minio/minio/internal/config" "github.com/minio/minio/internal/config/identity/openid/provider" "github.com/minio/minio/internal/hash/sha256" - "github.com/minio/pkg/v3/env" - xnet "github.com/minio/pkg/v3/net" - "github.com/minio/pkg/v3/policy" + "github.com/pgsty/silo-pkg/v3/env" + xnet "github.com/pgsty/silo-pkg/v3/net" + "github.com/pgsty/silo-pkg/v3/policy" ) // OpenID keys and envs. diff --git a/internal/config/identity/openid/providercfg.go b/internal/config/identity/openid/providercfg.go index 1ccc230cc..5bf643426 100644 --- a/internal/config/identity/openid/providercfg.go +++ b/internal/config/identity/openid/providercfg.go @@ -28,7 +28,7 @@ import ( "github.com/minio/minio/internal/config" "github.com/minio/minio/internal/config/identity/openid/provider" xhttp "github.com/minio/minio/internal/http" - xnet "github.com/minio/pkg/v3/net" + xnet "github.com/pgsty/silo-pkg/v3/net" ) type providerCfg struct { diff --git a/internal/config/identity/plugin/config.go b/internal/config/identity/plugin/config.go index 3b0ac830f..820e6dc3a 100644 --- a/internal/config/identity/plugin/config.go +++ b/internal/config/identity/plugin/config.go @@ -34,8 +34,8 @@ import ( "github.com/minio/minio/internal/arn" "github.com/minio/minio/internal/config" "github.com/minio/minio/internal/logger" - "github.com/minio/pkg/v3/env" - xnet "github.com/minio/pkg/v3/net" + "github.com/pgsty/silo-pkg/v3/env" + xnet "github.com/pgsty/silo-pkg/v3/net" ) func authNLogIf(ctx context.Context, err error) { diff --git a/internal/config/identity/tls/config.go b/internal/config/identity/tls/config.go index b002aab75..eb262a753 100644 --- a/internal/config/identity/tls/config.go +++ b/internal/config/identity/tls/config.go @@ -23,7 +23,7 @@ import ( "github.com/minio/minio/internal/auth" "github.com/minio/minio/internal/config" - "github.com/minio/pkg/v3/env" + "github.com/pgsty/silo-pkg/v3/env" ) const ( diff --git a/internal/config/ilm/ilm.go b/internal/config/ilm/ilm.go index cb88e6995..3fd62a44f 100644 --- a/internal/config/ilm/ilm.go +++ b/internal/config/ilm/ilm.go @@ -25,7 +25,7 @@ import ( "github.com/dustin/go-humanize" "github.com/minio/minio/internal/config" - "github.com/minio/pkg/v3/env" + "github.com/pgsty/silo-pkg/v3/env" ) // Errors returned when the access tiering configuration is unusable. diff --git a/internal/config/lambda/parse.go b/internal/config/lambda/parse.go index eac6a5def..bb46eae64 100644 --- a/internal/config/lambda/parse.go +++ b/internal/config/lambda/parse.go @@ -27,8 +27,8 @@ import ( "github.com/minio/minio/internal/config/lambda/event" "github.com/minio/minio/internal/config/lambda/target" "github.com/minio/minio/internal/logger" - "github.com/minio/pkg/v3/env" - xnet "github.com/minio/pkg/v3/net" + "github.com/pgsty/silo-pkg/v3/env" + xnet "github.com/pgsty/silo-pkg/v3/net" ) const ( diff --git a/internal/config/lambda/target/webhook.go b/internal/config/lambda/target/webhook.go index 20149f026..72246ddf8 100644 --- a/internal/config/lambda/target/webhook.go +++ b/internal/config/lambda/target/webhook.go @@ -32,8 +32,8 @@ import ( "github.com/minio/minio/internal/config/lambda/event" xhttp "github.com/minio/minio/internal/http" "github.com/minio/minio/internal/logger" - "github.com/minio/pkg/v3/certs" - xnet "github.com/minio/pkg/v3/net" + "github.com/pgsty/silo-pkg/v3/certs" + xnet "github.com/pgsty/silo-pkg/v3/net" ) // Webhook constants diff --git a/internal/config/notify/legacy.go b/internal/config/notify/legacy.go index d2a1d67f3..21417f0da 100644 --- a/internal/config/notify/legacy.go +++ b/internal/config/notify/legacy.go @@ -26,6 +26,25 @@ import ( "github.com/minio/minio/internal/event/target" ) +// LegacyDatabaseTargetError reports a pre-KV database notification target +// that cannot be migrated safely. It deliberately carries no configuration +// values so credentials cannot escape through startup logs. +type LegacyDatabaseTargetError struct { + subsystem string + target string + connectionKey string + invalid bool +} + +func (e *LegacyDatabaseTargetError) Error() string { + if e.invalid { + return fmt.Sprintf("%s:%s has invalid %s or target settings; fix the target before migrating to SILO", + e.subsystem, e.target, e.connectionKey) + } + return fmt.Sprintf("%s:%s requires %s; discrete database connection fields are not migrated to SILO", + e.subsystem, e.target, e.connectionKey) +} + // SetNotifyKafka - helper for config migration from older config. func SetNotifyKafka(s config.Config, name string, cfg target.KafkaArgs) error { if !cfg.Enable { @@ -325,8 +344,21 @@ func SetNotifyPostgres(s config.Config, psqName string, cfg target.PostgreSQLArg return nil } + if cfg.ConnectionString == "" { + return &LegacyDatabaseTargetError{ + subsystem: config.NotifyPostgresSubSys, + target: psqName, + connectionKey: target.PostgresConnectionString, + } + } + if err := cfg.Validate(); err != nil { - return err + return &LegacyDatabaseTargetError{ + subsystem: config.NotifyPostgresSubSys, + target: psqName, + connectionKey: target.PostgresConnectionString, + invalid: true, + } } s[config.NotifyPostgresSubSys][psqName] = config.KVS{ @@ -346,26 +378,6 @@ func SetNotifyPostgres(s config.Config, psqName string, cfg target.PostgreSQLArg Key: target.PostgresTable, Value: cfg.Table, }, - config.KV{ - Key: target.PostgresHost, - Value: cfg.Host.String(), - }, - config.KV{ - Key: target.PostgresPort, - Value: cfg.Port, - }, - config.KV{ - Key: target.PostgresUsername, - Value: cfg.Username, - }, - config.KV{ - Key: target.PostgresPassword, - Value: cfg.Password, - }, - config.KV{ - Key: target.PostgresDatabase, - Value: cfg.Database, - }, config.KV{ Key: target.PostgresQueueDir, Value: cfg.QueueDir, @@ -538,8 +550,21 @@ func SetNotifyMySQL(s config.Config, sqlName string, cfg target.MySQLArgs) error return nil } + if cfg.DSN == "" { + return &LegacyDatabaseTargetError{ + subsystem: config.NotifyMySQLSubSys, + target: sqlName, + connectionKey: target.MySQLDSNString, + } + } + if err := cfg.Validate(); err != nil { - return err + return &LegacyDatabaseTargetError{ + subsystem: config.NotifyMySQLSubSys, + target: sqlName, + connectionKey: target.MySQLDSNString, + invalid: true, + } } s[config.NotifyMySQLSubSys][sqlName] = config.KVS{ @@ -559,26 +584,6 @@ func SetNotifyMySQL(s config.Config, sqlName string, cfg target.MySQLArgs) error Key: target.MySQLTable, Value: cfg.Table, }, - config.KV{ - Key: target.MySQLHost, - Value: cfg.Host.String(), - }, - config.KV{ - Key: target.MySQLPort, - Value: cfg.Port, - }, - config.KV{ - Key: target.MySQLUsername, - Value: cfg.User, - }, - config.KV{ - Key: target.MySQLPassword, - Value: cfg.Password, - }, - config.KV{ - Key: target.MySQLDatabase, - Value: cfg.Database, - }, config.KV{ Key: target.MySQLQueueDir, Value: cfg.QueueDir, diff --git a/internal/config/notify/legacy_test.go b/internal/config/notify/legacy_test.go index 50f298d7f..5bfc8c207 100644 --- a/internal/config/notify/legacy_test.go +++ b/internal/config/notify/legacy_test.go @@ -18,14 +18,35 @@ package notify import ( + "errors" + "strings" "testing" "github.com/minio/minio/internal/config" "github.com/minio/minio/internal/event/target" - xnet "github.com/minio/pkg/v3/net" + xnet "github.com/pgsty/silo-pkg/v3/net" "github.com/rabbitmq/amqp091-go" ) +func assertLegacyDatabaseTargetError(t *testing.T, err error, subsystem, name, key string, secrets ...string) { + t.Helper() + var targetErr *LegacyDatabaseTargetError + if !errors.As(err, &targetErr) { + t.Fatalf("error = %v, want *LegacyDatabaseTargetError", err) + } + msg := err.Error() + for _, want := range []string{subsystem + config.SubSystemSeparator + name, key} { + if !strings.Contains(msg, want) { + t.Errorf("error %q does not contain %q", msg, want) + } + } + for _, secret := range secrets { + if secret != "" && strings.Contains(msg, secret) { + t.Errorf("error leaks configuration value %q: %s", secret, msg) + } + } +} + // T5 (NATS): a config produced by the legacy migration must survive validation // and round-trip back through the parser unchanged. Before the fix the // migration wrote an env var name as a config key, so every migrated NATS @@ -113,3 +134,172 @@ func TestSetNotifyAMQPRoundTrip(t *testing.T) { t.Errorf("Internal = true, want false (immediate must not be written to the internal key)") } } + +func TestSetNotifyDatabaseTargetsRequireConnectionStrings(t *testing.T) { + postgresHost, err := xnet.ParseHost("legacy-postgres.example") + if err != nil { + t.Fatal(err) + } + mysqlHost, err := xnet.ParseURL("legacy-mysql.example") + if err != nil { + t.Fatal(err) + } + + tests := []struct { + name string + subsystem string + key string + set func(config.Config) error + secrets []string + }{ + { + name: "postgres", + subsystem: config.NotifyPostgresSubSys, + key: target.PostgresConnectionString, + set: func(s config.Config) error { + return SetNotifyPostgres(s, testTargetName, target.PostgreSQLArgs{ + Enable: true, + Format: formatNamespace, + Table: "events", + Host: *postgresHost, + Port: "5432", + Username: "legacy-user", + Password: "legacy-postgres-password", + Database: "legacy-database", + }) + }, + secrets: []string{postgresHost.String(), "5432", "legacy-user", "legacy-postgres-password", "legacy-database"}, + }, + { + name: "mysql", + subsystem: config.NotifyMySQLSubSys, + key: target.MySQLDSNString, + set: func(s config.Config) error { + return SetNotifyMySQL(s, testTargetName, target.MySQLArgs{ + Enable: true, + Format: formatNamespace, + Table: "events", + Host: *mysqlHost, + Port: "3306", + User: "legacy-user", + Password: "legacy-mysql-password", + Database: "legacy-database", + }) + }, + secrets: []string{mysqlHost.String(), "3306", "legacy-user", "legacy-mysql-password", "legacy-database"}, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + s := config.Config{test.subsystem: map[string]config.KVS{}} + err := test.set(s) + assertLegacyDatabaseTargetError(t, err, test.subsystem, testTargetName, test.key, test.secrets...) + if _, ok := s[test.subsystem][testTargetName]; ok { + t.Fatal("unsupported target was emitted despite migration error") + } + }) + } +} + +func TestSetNotifyDisabledDatabaseTargetsAreIgnored(t *testing.T) { + s := config.Config{ + config.NotifyPostgresSubSys: map[string]config.KVS{}, + config.NotifyMySQLSubSys: map[string]config.KVS{}, + } + if err := SetNotifyPostgres(s, testTargetName, target.PostgreSQLArgs{Password: "discarded-postgres-secret"}); err != nil { + t.Fatalf("SetNotifyPostgres: %v", err) + } + if err := SetNotifyMySQL(s, testTargetName, target.MySQLArgs{Password: "discarded-mysql-secret"}); err != nil { + t.Fatalf("SetNotifyMySQL: %v", err) + } + if _, ok := s[config.NotifyPostgresSubSys][testTargetName]; ok { + t.Fatal("disabled Postgres target was emitted") + } + if _, ok := s[config.NotifyMySQLSubSys][testTargetName]; ok { + t.Fatal("disabled MySQL target was emitted") + } +} + +func TestSetNotifyInvalidDatabaseTargetsDoNotLeak(t *testing.T) { + tests := []struct { + name string + subsystem string + key string + secret string + set func(config.Config) error + }{ + { + name: "postgres", + subsystem: config.NotifyPostgresSubSys, + key: target.PostgresConnectionString, + secret: "postgres-dsn-secret", + set: func(s config.Config) error { + return SetNotifyPostgres(s, testTargetName, target.PostgreSQLArgs{ + Enable: true, + Format: formatNamespace, + ConnectionString: "host=db password=postgres-dsn-secret", + }) + }, + }, + { + name: "mysql", + subsystem: config.NotifyMySQLSubSys, + key: target.MySQLDSNString, + secret: "mysql-dsn-secret", + set: func(s config.Config) error { + return SetNotifyMySQL(s, testTargetName, target.MySQLArgs{ + Enable: true, + Format: formatNamespace, + DSN: "user:mysql-dsn-secret@tcp(db:3306/events", + Table: "events", + }) + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + s := config.Config{test.subsystem: map[string]config.KVS{}} + err := test.set(s) + assertLegacyDatabaseTargetError(t, err, test.subsystem, testTargetName, test.key, test.secret) + }) + } +} + +func TestDatabaseConnectionStringsSurviveKVTokenization(t *testing.T) { + tests := []struct { + name string + subsystem string + key string + input string + want string + }{ + { + name: "postgres", + subsystem: config.NotifyPostgresSubSys, + key: target.PostgresConnectionString, + input: `notify_postgres:dsn connection_string="host=db port=5432 dbname=events user=app password=inside" table="events"`, + want: "host=db port=5432 dbname=events user=app password=inside", + }, + { + name: "mysql", + subsystem: config.NotifyMySQLSubSys, + key: target.MySQLDSNString, + input: `notify_mysql:dsn dsn_string="user:pass@tcp(db:3306)/events?host=db&port=3306&password=inside" table="events"`, + want: "user:pass@tcp(db:3306)/events?host=db&port=3306&password=inside", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + s := config.Config{test.subsystem: map[string]config.KVS{}} + if _, err := s.SetKVS(test.input, DefaultNotificationKVS); err != nil { + t.Fatalf("SetKVS: %v", err) + } + if got := s[test.subsystem]["dsn"].Get(test.key); got != test.want { + t.Errorf("%s = %q, want %q", test.key, got, test.want) + } + }) + } +} diff --git a/internal/config/notify/parse.go b/internal/config/notify/parse.go index c4c7f7c93..31d1f7aed 100644 --- a/internal/config/notify/parse.go +++ b/internal/config/notify/parse.go @@ -32,8 +32,8 @@ import ( "github.com/minio/minio/internal/event" "github.com/minio/minio/internal/event/target" "github.com/minio/minio/internal/logger" - "github.com/minio/pkg/v3/env" - xnet "github.com/minio/pkg/v3/net" + "github.com/pgsty/silo-pkg/v3/env" + xnet "github.com/pgsty/silo-pkg/v3/net" "github.com/rabbitmq/amqp091-go" ) diff --git a/internal/config/notify/parse_test.go b/internal/config/notify/parse_test.go index 51e99747f..074461b1e 100644 --- a/internal/config/notify/parse_test.go +++ b/internal/config/notify/parse_test.go @@ -414,21 +414,9 @@ var configPkgConsts = map[string]string{ "Comment": config.Comment, } -// knownUnregisteredWrites records pre-existing instances of the exact defect -// this audit exists to catch: a legacy migration writing config keys that no -// default KVS registers, so the migrated config is rejected on the next load. -// -// These are inherited from upstream and are the same class as issue #39, but -// they are NOT part of the issue #39 fix and were left untouched deliberately. -// The Postgres/MySQL keys below are the pre-connection-string DSN fields; the -// migration still writes them and `password` carries a plaintext database -// password. -// -// This list must only ever shrink. Do not add entries to silence a new gap. -var knownUnregisteredWrites = map[string][]string{ - "SetNotifyPostgres": {"host", "port", "username", "password", "database"}, - "SetNotifyMySQL": {"host", "port", "username", "password", "database"}, -} +// knownUnregisteredWrites is a shrink-only ratchet for inherited migration +// gaps. Do not add entries to silence a new mismatch. +var knownUnregisteredWrites = map[string][]string{} func TestNotifyConfigKeysAreRegistered(t *testing.T) { targetConsts, err := parseTargetPkgStringConsts("../../event/target") diff --git a/internal/config/policy/opa/config.go b/internal/config/policy/opa/config.go index 47185fb87..0f91781ac 100644 --- a/internal/config/policy/opa/config.go +++ b/internal/config/policy/opa/config.go @@ -24,9 +24,9 @@ import ( "net/http" "github.com/minio/minio/internal/config" - "github.com/minio/pkg/v3/env" - xnet "github.com/minio/pkg/v3/net" - "github.com/minio/pkg/v3/policy" + "github.com/pgsty/silo-pkg/v3/env" + xnet "github.com/pgsty/silo-pkg/v3/net" + "github.com/pgsty/silo-pkg/v3/policy" ) // Env IAM OPA URL diff --git a/internal/config/policy/plugin/config.go b/internal/config/policy/plugin/config.go index 6e651c5ad..bbf28e0ef 100644 --- a/internal/config/policy/plugin/config.go +++ b/internal/config/policy/plugin/config.go @@ -26,8 +26,8 @@ import ( "github.com/minio/minio/internal/config" xhttp "github.com/minio/minio/internal/http" - xnet "github.com/minio/pkg/v3/net" - "github.com/minio/pkg/v3/policy" + xnet "github.com/pgsty/silo-pkg/v3/net" + "github.com/pgsty/silo-pkg/v3/policy" ) // Authorization Plugin config and env variables diff --git a/internal/config/scanner/scanner.go b/internal/config/scanner/scanner.go index 7d1714139..00248ceea 100644 --- a/internal/config/scanner/scanner.go +++ b/internal/config/scanner/scanner.go @@ -23,7 +23,7 @@ import ( "time" "github.com/minio/minio/internal/config" - "github.com/minio/pkg/v3/env" + "github.com/pgsty/silo-pkg/v3/env" ) // Compression environment variables diff --git a/internal/config/storageclass/storage-class.go b/internal/config/storageclass/storage-class.go index 18121141a..52bca35eb 100644 --- a/internal/config/storageclass/storage-class.go +++ b/internal/config/storageclass/storage-class.go @@ -28,7 +28,7 @@ import ( "github.com/dustin/go-humanize" "github.com/minio/minio/internal/config" "github.com/minio/minio/internal/logger" - "github.com/minio/pkg/v3/env" + "github.com/pgsty/silo-pkg/v3/env" ) // Standard constants for all storage class diff --git a/internal/config/subnet/config.go b/internal/config/subnet/config.go index 3665add3a..706608573 100644 --- a/internal/config/subnet/config.go +++ b/internal/config/subnet/config.go @@ -25,8 +25,8 @@ import ( "sync" "github.com/minio/minio/internal/config" - "github.com/minio/pkg/v3/env" - xnet "github.com/minio/pkg/v3/net" + "github.com/pgsty/silo-pkg/v3/env" + xnet "github.com/pgsty/silo-pkg/v3/net" ) // DefaultKVS - default KV config for subnet settings diff --git a/internal/crypto/auto-encryption.go b/internal/crypto/auto-encryption.go index f2cdcc5c5..c8be8c4d1 100644 --- a/internal/crypto/auto-encryption.go +++ b/internal/crypto/auto-encryption.go @@ -19,7 +19,7 @@ package crypto import ( "github.com/minio/minio/internal/config" - "github.com/minio/pkg/v3/env" + "github.com/pgsty/silo-pkg/v3/env" ) const ( diff --git a/internal/disk/directio_unix.go b/internal/disk/directio_unix.go index 883df95d0..a31fc6bd3 100644 --- a/internal/disk/directio_unix.go +++ b/internal/disk/directio_unix.go @@ -43,7 +43,7 @@ func DisableDirectIO(f *os.File) error { if err != nil { return err } - flag &= ^(syscall.O_DIRECT) + flag &= ^syscall.O_DIRECT _, err = unix.FcntlInt(fd, unix.F_SETFL, flag) return err } diff --git a/internal/dsync/drwmutex.go b/internal/dsync/drwmutex.go index 7d6506eae..f8e4f95fd 100644 --- a/internal/dsync/drwmutex.go +++ b/internal/dsync/drwmutex.go @@ -29,8 +29,8 @@ import ( xioutil "github.com/minio/minio/internal/ioutil" "github.com/minio/minio/internal/mcontext" - "github.com/minio/pkg/v3/console" - "github.com/minio/pkg/v3/env" + "github.com/pgsty/silo-pkg/v3/console" + "github.com/pgsty/silo-pkg/v3/env" ) // Indicator if logging is enabled. diff --git a/internal/event/rules.go b/internal/event/rules.go index 0218aabc6..bd3b99718 100644 --- a/internal/event/rules.go +++ b/internal/event/rules.go @@ -20,7 +20,7 @@ package event import ( "strings" - "github.com/minio/pkg/v3/wildcard" + "github.com/pgsty/silo-pkg/v3/wildcard" ) // NewPattern - create new pattern for prefix/suffix. diff --git a/internal/event/target/amqp.go b/internal/event/target/amqp.go index 8b001f5db..066cae5ad 100644 --- a/internal/event/target/amqp.go +++ b/internal/event/target/amqp.go @@ -32,7 +32,7 @@ import ( "github.com/minio/minio/internal/logger" "github.com/minio/minio/internal/once" "github.com/minio/minio/internal/store" - xnet "github.com/minio/pkg/v3/net" + xnet "github.com/pgsty/silo-pkg/v3/net" "github.com/rabbitmq/amqp091-go" ) diff --git a/internal/event/target/elasticsearch.go b/internal/event/target/elasticsearch.go index bccb873db..63611e779 100644 --- a/internal/event/target/elasticsearch.go +++ b/internal/event/target/elasticsearch.go @@ -38,7 +38,7 @@ import ( "github.com/minio/minio/internal/logger" "github.com/minio/minio/internal/once" "github.com/minio/minio/internal/store" - xnet "github.com/minio/pkg/v3/net" + xnet "github.com/pgsty/silo-pkg/v3/net" "github.com/pkg/errors" ) diff --git a/internal/event/target/kafka.go b/internal/event/target/kafka.go index d6af69b8b..8f07dfd6e 100644 --- a/internal/event/target/kafka.go +++ b/internal/event/target/kafka.go @@ -35,7 +35,7 @@ import ( "github.com/minio/minio/internal/logger" "github.com/minio/minio/internal/once" "github.com/minio/minio/internal/store" - xnet "github.com/minio/pkg/v3/net" + xnet "github.com/pgsty/silo-pkg/v3/net" "github.com/IBM/sarama" saramatls "github.com/IBM/sarama/tools/tls" diff --git a/internal/event/target/mqtt.go b/internal/event/target/mqtt.go index 8f568cd3a..a9751ed2d 100644 --- a/internal/event/target/mqtt.go +++ b/internal/event/target/mqtt.go @@ -33,7 +33,7 @@ import ( "github.com/minio/minio/internal/logger" "github.com/minio/minio/internal/once" "github.com/minio/minio/internal/store" - xnet "github.com/minio/pkg/v3/net" + xnet "github.com/pgsty/silo-pkg/v3/net" ) const ( diff --git a/internal/event/target/mysql.go b/internal/event/target/mysql.go index 0f311232a..e2c68bf93 100644 --- a/internal/event/target/mysql.go +++ b/internal/event/target/mysql.go @@ -35,7 +35,7 @@ import ( "github.com/minio/minio/internal/logger" "github.com/minio/minio/internal/once" "github.com/minio/minio/internal/store" - xnet "github.com/minio/pkg/v3/net" + xnet "github.com/pgsty/silo-pkg/v3/net" ) const ( @@ -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" diff --git a/internal/event/target/nats.go b/internal/event/target/nats.go index f205a1713..712acb070 100644 --- a/internal/event/target/nats.go +++ b/internal/event/target/nats.go @@ -33,9 +33,9 @@ import ( "github.com/minio/minio/internal/logger" "github.com/minio/minio/internal/once" "github.com/minio/minio/internal/store" - xnet "github.com/minio/pkg/v3/net" "github.com/nats-io/nats.go" "github.com/nats-io/stan.go" + xnet "github.com/pgsty/silo-pkg/v3/net" ) // NATS related constants diff --git a/internal/event/target/nats_contrib_test.go b/internal/event/target/nats_contrib_test.go index 42a5f0609..0cfa6704a 100644 --- a/internal/event/target/nats_contrib_test.go +++ b/internal/event/target/nats_contrib_test.go @@ -21,8 +21,8 @@ import ( "github.com/nats-io/nats-server/v2/server" - xnet "github.com/minio/pkg/v3/net" natsserver "github.com/nats-io/nats-server/v2/test" + xnet "github.com/pgsty/silo-pkg/v3/net" ) func TestNatsConnPlain(t *testing.T) { @@ -35,7 +35,7 @@ func TestNatsConnPlain(t *testing.T) { Enable: true, Address: xnet.Host{ Name: "localhost", - Port: (xnet.Port(opts.Port)), + Port: xnet.Port(opts.Port), IsPortSet: true, }, Subject: "test", @@ -59,7 +59,7 @@ func TestNatsConnUserPass(t *testing.T) { Enable: true, Address: xnet.Host{ Name: "localhost", - Port: (xnet.Port(opts.Port)), + Port: xnet.Port(opts.Port), IsPortSet: true, }, Subject: "test", @@ -85,7 +85,7 @@ func TestNatsConnToken(t *testing.T) { Enable: true, Address: xnet.Host{ Name: "localhost", - Port: (xnet.Port(opts.Port)), + Port: xnet.Port(opts.Port), IsPortSet: true, }, Subject: "test", @@ -116,7 +116,7 @@ func TestNatsConnNKeySeed(t *testing.T) { Enable: true, Address: xnet.Host{ Name: "localhost", - Port: (xnet.Port(opts.Port)), + Port: xnet.Port(opts.Port), IsPortSet: true, }, Subject: "test", diff --git a/internal/event/target/nats_tls_contrib_test.go b/internal/event/target/nats_tls_contrib_test.go index 30cf5b46b..49f331157 100644 --- a/internal/event/target/nats_tls_contrib_test.go +++ b/internal/event/target/nats_tls_contrib_test.go @@ -21,8 +21,8 @@ import ( "path/filepath" "testing" - xnet "github.com/minio/pkg/v3/net" natsserver "github.com/nats-io/nats-server/v2/test" + xnet "github.com/pgsty/silo-pkg/v3/net" ) func TestNatsConnTLSCustomCA(t *testing.T) { @@ -33,7 +33,7 @@ func TestNatsConnTLSCustomCA(t *testing.T) { Enable: true, Address: xnet.Host{ Name: "localhost", - Port: (xnet.Port(opts.Port)), + Port: xnet.Port(opts.Port), IsPortSet: true, }, Subject: "test", @@ -56,7 +56,7 @@ func TestNatsConnTLSCustomCAHandshakeFirst(t *testing.T) { Enable: true, Address: xnet.Host{ Name: "localhost", - Port: (xnet.Port(opts.Port)), + Port: xnet.Port(opts.Port), IsPortSet: true, }, Subject: "test", @@ -80,7 +80,7 @@ func TestNatsConnTLSClientAuthorization(t *testing.T) { Enable: true, Address: xnet.Host{ Name: "localhost", - Port: (xnet.Port(opts.Port)), + Port: xnet.Port(opts.Port), IsPortSet: true, }, Subject: "test", diff --git a/internal/event/target/nsq.go b/internal/event/target/nsq.go index ec04d9937..a145cfe8b 100644 --- a/internal/event/target/nsq.go +++ b/internal/event/target/nsq.go @@ -33,7 +33,7 @@ import ( "github.com/minio/minio/internal/logger" "github.com/minio/minio/internal/once" "github.com/minio/minio/internal/store" - xnet "github.com/minio/pkg/v3/net" + xnet "github.com/pgsty/silo-pkg/v3/net" ) // NSQ constants diff --git a/internal/event/target/nsq_test.go b/internal/event/target/nsq_test.go index 32926ab58..f60b2d618 100644 --- a/internal/event/target/nsq_test.go +++ b/internal/event/target/nsq_test.go @@ -20,7 +20,7 @@ package target import ( "testing" - xnet "github.com/minio/pkg/v3/net" + xnet "github.com/pgsty/silo-pkg/v3/net" ) func TestNSQArgs_Validate(t *testing.T) { diff --git a/internal/event/target/postgresql.go b/internal/event/target/postgresql.go index 228a2720c..bf7803db3 100644 --- a/internal/event/target/postgresql.go +++ b/internal/event/target/postgresql.go @@ -38,7 +38,7 @@ import ( "github.com/minio/minio/internal/logger" "github.com/minio/minio/internal/once" "github.com/minio/minio/internal/store" - xnet "github.com/minio/pkg/v3/net" + xnet "github.com/pgsty/silo-pkg/v3/net" ) const ( @@ -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" diff --git a/internal/event/target/redis.go b/internal/event/target/redis.go index 53082a515..e4c4373bb 100644 --- a/internal/event/target/redis.go +++ b/internal/event/target/redis.go @@ -33,7 +33,7 @@ import ( "github.com/minio/minio/internal/logger" "github.com/minio/minio/internal/once" "github.com/minio/minio/internal/store" - xnet "github.com/minio/pkg/v3/net" + xnet "github.com/pgsty/silo-pkg/v3/net" ) // Redis constants diff --git a/internal/event/target/webhook.go b/internal/event/target/webhook.go index e5dc4f699..c0b87cbfa 100644 --- a/internal/event/target/webhook.go +++ b/internal/event/target/webhook.go @@ -38,8 +38,8 @@ import ( "github.com/minio/minio/internal/logger" "github.com/minio/minio/internal/once" "github.com/minio/minio/internal/store" - "github.com/minio/pkg/v3/certs" - xnet "github.com/minio/pkg/v3/net" + "github.com/pgsty/silo-pkg/v3/certs" + xnet "github.com/pgsty/silo-pkg/v3/net" ) // Webhook constants diff --git a/internal/event/targetlist.go b/internal/event/targetlist.go index 3aeee5d26..3b8e2d48a 100644 --- a/internal/event/targetlist.go +++ b/internal/event/targetlist.go @@ -27,7 +27,7 @@ import ( "github.com/minio/minio/internal/logger" "github.com/minio/minio/internal/store" - "github.com/minio/pkg/v3/workers" + "github.com/pgsty/silo-pkg/v3/workers" ) const ( diff --git a/internal/grid/connection.go b/internal/grid/connection.go index 576f4229a..bf8a1fe9d 100644 --- a/internal/grid/connection.go +++ b/internal/grid/connection.go @@ -41,7 +41,7 @@ import ( xioutil "github.com/minio/minio/internal/ioutil" "github.com/minio/minio/internal/logger" "github.com/minio/minio/internal/pubsub" - xnet "github.com/minio/pkg/v3/net" + xnet "github.com/pgsty/silo-pkg/v3/net" "github.com/puzpuzpuz/xsync/v3" "github.com/tinylib/msgp/msgp" "github.com/zeebo/xxh3" @@ -1806,8 +1806,8 @@ func (ww *wsWriter) writeFrame(w io.Writer, f ws.Frame) error { const ( bit0 = 0x80 len7 = int64(125) - len16 = int64(^(uint16(0))) - len64 = int64(^(uint64(0)) >> 1) + len16 = int64(^uint16(0)) + len64 = int64(^uint64(0) >> 1) ) bts := ww.tmp[:] diff --git a/internal/handlers/proxy.go b/internal/handlers/proxy.go index 028481858..fbc32fd1c 100644 --- a/internal/handlers/proxy.go +++ b/internal/handlers/proxy.go @@ -28,7 +28,7 @@ import ( "strings" "github.com/minio/minio/internal/config" - "github.com/minio/pkg/v3/env" + "github.com/pgsty/silo-pkg/v3/env" ) var ( diff --git a/internal/hash/checksum.go b/internal/hash/checksum.go index fbf62fd84..f8238c506 100644 --- a/internal/hash/checksum.go +++ b/internal/hash/checksum.go @@ -157,7 +157,6 @@ func ChecksumStringToType(alg string) ChecksumType { case "SHA256": return ChecksumSHA256 case "CRC64NVME": - // AWS seems to ignore full value, and just assume it. return ChecksumCRC64NVME case "": return ChecksumNone @@ -192,7 +191,9 @@ func NewChecksumType(alg, objType string) ChecksumType { } return ChecksumSHA256 case "CRC64NVME": - // AWS seems to ignore full value, and just assume it. + if objType == xhttp.AmzChecksumTypeComposite { + return ChecksumInvalid + } return ChecksumCRC64NVME case "": if full != 0 { @@ -247,7 +248,7 @@ func (c ChecksumType) StringFull() string { // FullObjectRequested will return if the checksum type indicates full object checksum was requested. func (c ChecksumType) FullObjectRequested() bool { - return c&(ChecksumFullObject) == ChecksumFullObject || c.Is(ChecksumCRC64NVME) + return c&ChecksumFullObject == ChecksumFullObject || c.Is(ChecksumCRC64NVME) } // IsMultipartComposite returns true if the checksum is multipart and full object was not requested. @@ -657,22 +658,73 @@ func AddChecksumHeader(w http.ResponseWriter, c map[string]string) { } } +func isSupportedChecksumHeader(name string) bool { + switch { + case strings.EqualFold(name, xhttp.AmzChecksumAlgo), + strings.EqualFold(name, xhttp.AmzChecksumType), + strings.EqualFold(name, xhttp.AmzChecksumMode): + return true + } + for _, checksumType := range BaseChecksumTypes { + if strings.EqualFold(name, checksumType.Key()) { + return true + } + } + return false +} + +func hasUnsupportedChecksumHeader(h http.Header) bool { + for name := range h { + if strings.HasPrefix(strings.ToLower(name), "x-amz-checksum-") && !isSupportedChecksumHeader(name) { + return true + } + } + return false +} + // GetContentChecksum returns content checksum. // Returns ErrInvalidChecksum if so. // Returns nil, nil if no checksum. func GetContentChecksum(h http.Header) (*Checksum, error) { + if hasUnsupportedChecksumHeader(h) { + return nil, ErrInvalidChecksum + } if trailing := h.Values(xhttp.AmzTrailer); len(trailing) > 0 { var res *Checksum - for _, header := range trailing { - var duplicates bool - for _, t := range BaseChecksumTypes { - if strings.EqualFold(t.Key(), header) { - duplicates = res != nil - res = NewChecksumWithType(t|ChecksumTrailing, "") + for _, headers := range trailing { + for header := range strings.SplitSeq(headers, ",") { + header = strings.TrimSpace(header) + var duplicates bool + for _, t := range BaseChecksumTypes { + if strings.EqualFold(t.Key(), header) { + duplicates = res != nil + // A checksum can be advertised via x-amz-trailer while its + // value is still delivered in the request headers. The AWS + // Java SDK v2 does this on chunked (aws-chunked) uploads: + // it sends STREAMING-AWS4-HMAC-SHA256-PAYLOAD (no trailer), + // puts the precomputed value in x-amz-checksum-*, yet still + // lists it in x-amz-trailer, so no trailer ever arrives. + // When the value is present as a header, honor it directly + // instead of waiting for a trailer that will never be read. + if v := h.Get(t.Key()); v != "" { + res = NewChecksumWithType(t, v) + if res == nil { + // The value is supplied in the header but does + // not parse. A malformed client-supplied checksum + // is an error, not a reason to skip validation. + return nil, ErrInvalidChecksum + } + } else { + res = NewChecksumWithType(t|ChecksumTrailing, "") + } + } + } + if strings.HasPrefix(strings.ToLower(header), "x-amz-checksum-") && !isSupportedChecksumHeader(header) { + return nil, ErrInvalidChecksum + } + if duplicates { + return nil, ErrInvalidChecksum } - } - if duplicates { - return nil, ErrInvalidChecksum } } if res != nil { @@ -682,7 +734,11 @@ func GetContentChecksum(h http.Header) (*Checksum, error) { return nil, ErrInvalidChecksum } res.Type |= ChecksumFullObject - case xhttp.AmzChecksumTypeComposite, "": + case xhttp.AmzChecksumTypeComposite: + if res.Type.Base().Is(ChecksumCRC64NVME) { + return nil, ErrInvalidChecksum + } + case "": default: return nil, ErrInvalidChecksum } @@ -748,5 +804,8 @@ func getContentChecksum(h http.Header) (t ChecksumType, s string) { for _, t := range BaseChecksumTypes { checkType(t) } + if t.Base().Is(ChecksumCRC64NVME) && h.Get(xhttp.AmzChecksumType) == xhttp.AmzChecksumTypeComposite { + return ChecksumInvalid, "" + } return t, s } diff --git a/internal/hash/checksum_test.go b/internal/hash/checksum_test.go index 504803795..f033db20e 100644 --- a/internal/hash/checksum_test.go +++ b/internal/hash/checksum_test.go @@ -18,14 +18,58 @@ package hash import ( + "errors" + "net/http" "net/http/httptest" "testing" xhttp "github.com/minio/minio/internal/http" ) +func TestGetContentChecksumRejectsUnsupportedHeaders(t *testing.T) { + unsupported := []string{ + "x-amz-checksum-md5", + "x-amz-checksum-sha512", + "x-amz-checksum-xxhash64", + "x-amz-checksum-xxhash3", + "x-amz-checksum-xxhash128", + "x-amz-checksum-future", + } + for _, header := range unsupported { + t.Run("header/"+header, func(t *testing.T) { + h := http.Header{header: {"AA=="}} + if _, err := GetContentChecksum(h); !errors.Is(err, ErrInvalidChecksum) { + t.Fatalf("GetContentChecksum(%s) error = %v, want ErrInvalidChecksum", header, err) + } + }) + t.Run("trailer/"+header, func(t *testing.T) { + h := http.Header{xhttp.AmzTrailer: {header}} + if _, err := GetContentChecksum(h); !errors.Is(err, ErrInvalidChecksum) { + t.Fatalf("GetContentChecksum(trailer %s) error = %v, want ErrInvalidChecksum", header, err) + } + }) + } + + for header, value := range map[string]string{ + xhttp.AmzChecksumAlgo: "CRC32", + xhttp.AmzChecksumType: xhttp.AmzChecksumTypeComposite, + xhttp.AmzChecksumMode: "ENABLED", + "x-amz-sdk-checksum-algorithm": "SHA512", + } { + t.Run("control/"+header, func(t *testing.T) { + h := http.Header{header: {value}} + if _, err := GetContentChecksum(h); errors.Is(err, ErrInvalidChecksum) { + t.Fatalf("control header %s was rejected", header) + } + }) + } +} + // TestChecksumAddToHeader tests that adding and retrieving a checksum on a header works func TestChecksumAddToHeader(t *testing.T) { + if got := NewChecksumType("CRC64NVME", xhttp.AmzChecksumTypeComposite); !got.Is(ChecksumInvalid) { + t.Fatalf("CRC64NVME/COMPOSITE = %s, want invalid", got.StringFull()) + } tests := []struct { name string checksum ChecksumType @@ -106,6 +150,16 @@ func TestChecksumAddToHeader(t *testing.T) { } } +func TestCRC64NVMECompositeTrailerIsInvalid(t *testing.T) { + h := http.Header{} + h.Set(xhttp.AmzTrailer, ChecksumCRC64NVME.Key()) + h.Set(xhttp.AmzChecksumType, xhttp.AmzChecksumTypeComposite) + _, err := GetContentChecksum(h) + if !errors.Is(err, ErrInvalidChecksum) { + t.Fatalf("CRC64NVME/COMPOSITE trailer error = %v, want ErrInvalidChecksum", err) + } +} + // TestChecksumSerializeDeserialize checks AppendTo can be reversed by ChecksumFromBytes func TestChecksumSerializeDeserialize(t *testing.T) { myData := []byte("this-is-a-checksum-data-test") @@ -203,3 +257,72 @@ func TestChecksumSerializeDeserializeMultiPart(t *testing.T) { } } } + +// TestGetContentChecksumTrailerWithHeaderValue covers the case where a checksum +// is advertised via x-amz-trailer while its value is delivered as a request +// header (no trailer is actually sent). The AWS Java SDK v2 does this on chunked +// (aws-chunked) uploads that use STREAMING-AWS4-HMAC-SHA256-PAYLOAD (non-trailer) +// but still list the checksum in x-amz-trailer. See issue #107. The header value +// must be honored as a non-trailing checksum instead of being treated as an empty +// trailing checksum. +func TestGetContentChecksumTrailerWithHeaderValue(t *testing.T) { + const crc = "Hkksgg==" // CRC32 of "Hello CRC32!" + + // Trailer advertised AND value present in header -> non-trailing, value honored. + h := http.Header{} + h.Set(xhttp.AmzTrailer, xhttp.AmzChecksumCRC32) + h.Set(xhttp.AmzChecksumCRC32, crc) + cs, err := GetContentChecksum(h) + if err != nil { + t.Fatalf("GetContentChecksum error = %v, want nil", err) + } + if cs == nil { + t.Fatal("GetContentChecksum returned nil checksum") + } + if cs.Type.Trailing() { + t.Errorf("checksum reported as trailing; want non-trailing since value is in the header") + } + if !cs.Type.Is(ChecksumCRC32) { + t.Errorf("checksum type = %s, want CRC32", cs.Type.StringFull()) + } + if cs.Encoded != crc { + t.Errorf("checksum value = %q, want %q", cs.Encoded, crc) + } + + // Trailer advertised WITHOUT a header value -> stays trailing (unchanged). + h2 := http.Header{} + h2.Set(xhttp.AmzTrailer, xhttp.AmzChecksumCRC32) + cs2, err := GetContentChecksum(h2) + if err != nil { + t.Fatalf("GetContentChecksum (no header value) error = %v, want nil", err) + } + if cs2 == nil || !cs2.Type.Trailing() { + t.Errorf("checksum = %v, want a trailing CRC32 checksum", cs2) + } +} + +// TestGetContentChecksumTrailerMalformedHeaderValue guards against turning a +// malformed client-supplied checksum into a no-op. When a checksum is advertised +// via x-amz-trailer and its header value is present but does not parse, the +// request must be rejected (ErrInvalidChecksum) rather than silently dropped. +// The mismatched x-amz-checksum-algorithm selector makes the regression visible: +// without the guard, execution falls through to getContentChecksum which would +// return (nil, nil) and install no validator at all. +func TestGetContentChecksumTrailerMalformedHeaderValue(t *testing.T) { + h := http.Header{} + h.Set(xhttp.AmzTrailer, xhttp.AmzChecksumCRC32) + h.Set(xhttp.AmzChecksumCRC32, "AQID") // decodes to 3 bytes -> invalid CRC32 + h.Set(xhttp.AmzChecksumAlgo, "SHA256") + cs, err := GetContentChecksum(h) + if !errors.Is(err, ErrInvalidChecksum) { + t.Fatalf("GetContentChecksum error = %v (checksum %v), want ErrInvalidChecksum", err, cs) + } + + // Same, without the misleading algorithm selector: still an error. + h2 := http.Header{} + h2.Set(xhttp.AmzTrailer, xhttp.AmzChecksumCRC32) + h2.Set(xhttp.AmzChecksumCRC32, "AQID") + if _, err := GetContentChecksum(h2); !errors.Is(err, ErrInvalidChecksum) { + t.Fatalf("GetContentChecksum (no algo selector) error = %v, want ErrInvalidChecksum", err) + } +} diff --git a/internal/http/server_test.go b/internal/http/server_test.go index 27f260aed..425bedc3c 100644 --- a/internal/http/server_test.go +++ b/internal/http/server_test.go @@ -24,7 +24,7 @@ import ( "reflect" "testing" - "github.com/minio/pkg/v3/certs" + "github.com/pgsty/silo-pkg/v3/certs" ) func TestNewServer(t *testing.T) { diff --git a/internal/http/transports.go b/internal/http/transports.go index fba86bd32..d0741268a 100644 --- a/internal/http/transports.go +++ b/internal/http/transports.go @@ -25,7 +25,7 @@ import ( "syscall" "time" - "github.com/minio/pkg/v3/certs" + "github.com/pgsty/silo-pkg/v3/certs" ) // tlsClientSessionCacheSize is the cache size for client sessions. diff --git a/internal/ioutil/wait_pipe.go b/internal/ioutil/wait_pipe.go index 67f490ba4..ce1620929 100644 --- a/internal/ioutil/wait_pipe.go +++ b/internal/ioutil/wait_pipe.go @@ -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 } diff --git a/internal/kms/config.go b/internal/kms/config.go index a319e6cb9..1a5fe2b06 100644 --- a/internal/kms/config.go +++ b/internal/kms/config.go @@ -36,9 +36,9 @@ import ( "aead.dev/mtls" "github.com/minio/kms-go/kes" "github.com/minio/kms-go/kms" - "github.com/minio/pkg/v3/certs" - "github.com/minio/pkg/v3/ellipses" - "github.com/minio/pkg/v3/env" + "github.com/pgsty/silo-pkg/v3/certs" + "github.com/pgsty/silo-pkg/v3/ellipses" + "github.com/pgsty/silo-pkg/v3/env" ) // Environment variables for MinIO KMS. diff --git a/internal/kms/stub.go b/internal/kms/stub.go index 154df2cfb..a4bff73a0 100644 --- a/internal/kms/stub.go +++ b/internal/kms/stub.go @@ -25,7 +25,7 @@ import ( "time" "github.com/minio/madmin-go/v3" - "github.com/minio/pkg/v3/wildcard" + "github.com/pgsty/silo-pkg/v3/wildcard" ) var ( diff --git a/internal/logger/config.go b/internal/logger/config.go index 5cc71d0d9..dab1b7278 100644 --- a/internal/logger/config.go +++ b/internal/logger/config.go @@ -26,8 +26,8 @@ import ( "strings" "time" - "github.com/minio/pkg/v3/env" - xnet "github.com/minio/pkg/v3/net" + "github.com/pgsty/silo-pkg/v3/env" + xnet "github.com/pgsty/silo-pkg/v3/net" "github.com/minio/minio/internal/config" "github.com/minio/minio/internal/logger/target/http" diff --git a/internal/logger/target/http/http.go b/internal/logger/target/http/http.go index 0b0277845..1deb98666 100644 --- a/internal/logger/target/http/http.go +++ b/internal/logger/target/http/http.go @@ -38,7 +38,7 @@ import ( types "github.com/minio/minio/internal/logger/target/loggertypes" "github.com/minio/minio/internal/once" "github.com/minio/minio/internal/store" - xnet "github.com/minio/pkg/v3/net" + xnet "github.com/pgsty/silo-pkg/v3/net" "github.com/valyala/bytebufferpool" ) diff --git a/internal/logger/target/kafka/kafka.go b/internal/logger/target/kafka/kafka.go index 4720f85d2..34940ffc9 100644 --- a/internal/logger/target/kafka/kafka.go +++ b/internal/logger/target/kafka/kafka.go @@ -38,7 +38,7 @@ import ( types "github.com/minio/minio/internal/logger/target/loggertypes" "github.com/minio/minio/internal/once" "github.com/minio/minio/internal/store" - xnet "github.com/minio/pkg/v3/net" + xnet "github.com/pgsty/silo-pkg/v3/net" ) // the suffix for the configured queue dir where the logs will be persisted. diff --git a/internal/rest/client.go b/internal/rest/client.go index e115bdc57..56adbca19 100644 --- a/internal/rest/client.go +++ b/internal/rest/client.go @@ -37,7 +37,7 @@ import ( xhttp "github.com/minio/minio/internal/http" "github.com/minio/minio/internal/logger" "github.com/minio/minio/internal/mcontext" - xnet "github.com/minio/pkg/v3/net" + xnet "github.com/pgsty/silo-pkg/v3/net" ) const logSubsys = "internodes" diff --git a/internal/s3select/message.go b/internal/s3select/message.go index e2ed15945..7aa93c23c 100644 --- a/internal/s3select/message.go +++ b/internal/s3select/message.go @@ -295,7 +295,15 @@ func (writer *messageWriter) start() { select { case data := <-writer.errCh: quitFlag = true - // Flush collected records before sending error message + // A record accepted by SendRecord may still be queued when the + // error arrives, because select picks between the two channels + // at random. Stage it first so every record produced before the + // error precedes the error message instead of being dropped. + for len(writer.payloadCh) > 0 { + if !writer.stageRecord(<-writer.payloadCh) { + break + } + } if !writer.flushRecords() { break } @@ -316,23 +324,8 @@ func (writer *messageWriter) start() { break } writer.write(endMessage) - } else { - for payload.Len() > 0 { - copiedLen := copy(writer.payloadBuffer[writer.payloadBufferIndex:], payload.Bytes()) - writer.payloadBufferIndex += copiedLen - payload.Next(copiedLen) - - // If buffer is filled, flush it now! - freeSpace := bufLength - writer.payloadBufferIndex - if freeSpace == 0 { - if !writer.flushRecords() { - quitFlag = true - break - } - } - } - - bufPool.Put(payload) + } else if !writer.stageRecord(payload) { + quitFlag = true } case <-recordStagingTicker.C: @@ -368,6 +361,26 @@ func (writer *messageWriter) start() { } } +// stageRecord copies a record into the payload buffer, flushing whenever +// the buffer fills, and returns the buffer to the pool. It reports false when +// a flush failed. +func (writer *messageWriter) stageRecord(payload *bytes.Buffer) bool { + defer bufPool.Put(payload) + for payload.Len() > 0 { + copiedLen := copy(writer.payloadBuffer[writer.payloadBufferIndex:], payload.Bytes()) + writer.payloadBufferIndex += copiedLen + payload.Next(copiedLen) + + // If buffer is filled, flush it now! + if bufLength-writer.payloadBufferIndex == 0 { + if !writer.flushRecords() { + return false + } + } + } + return true +} + // Sends a single whole record. func (writer *messageWriter) SendRecord(payload *bytes.Buffer) error { select { diff --git a/internal/s3select/message_test.go b/internal/s3select/message_test.go new file mode 100644 index 000000000..e406ee04d --- /dev/null +++ b/internal/s3select/message_test.go @@ -0,0 +1,193 @@ +// Copyright (c) 2026 PGSTY +// +// This file is part of MinIO Object Storage stack +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package s3select + +import ( + "bytes" + "io" + "net/http" + "strings" + "testing" + + "github.com/minio/minio-go/v7" +) + +// decodeEvents parses a recorded event stream with the minio-go client and +// returns the concatenated record payloads and the terminal error, if any. +func decodeEvents(t *testing.T, response []byte) ([]byte, error) { + t.Helper() + body := &testResponseBody{Reader: bytes.NewReader(response), closed: make(chan struct{})} + res, err := minio.NewSelectResults(&http.Response{StatusCode: http.StatusOK, Body: body, ContentLength: int64(len(response))}, "testbucket") + if err != nil { + t.Fatal(err) + } + records, readErr := io.ReadAll(res) + <-body.closed + return records, readErr +} + +func newTestRecord(content string) *bytes.Buffer { + payload := bufPool.Get() + payload.Reset() + payload.WriteString(content) + return payload +} + +// eventOrder returns the byte offsets of the given markers in the response, +// or -1 for a marker that is absent. +func eventOrder(response []byte, markers ...string) []int { + offsets := make([]int, len(markers)) + for i, marker := range markers { + offsets[i] = bytes.Index(response, []byte(marker)) + } + return offsets +} + +func ascending(offsets []int) bool { + for i, offset := range offsets { + if offset < 0 || (i > 0 && offset <= offsets[i-1]) { + return false + } + } + return true +} + +// TestMessageWriterFlushesQueuedRecordBeforeError sends a record and an error +// back to back, so the writer goroutine sees both channels ready and picks +// one at random. The record must appear in the response before the error +// message every time. +func TestMessageWriterFlushesQueuedRecordBeforeError(t *testing.T) { + for i := range 200 { + w := &testResponseWriter{} + writer := newMessageWriter(w, nil) + payload := bufPool.Get() + payload.Reset() + payload.WriteString(`{"id":1}` + "\n") + if err := writer.SendRecord(payload); err != nil { + t.Fatalf("run %d: SendRecord: %v", i, err) + } + if err := writer.FinishWithError("OverMaxRecordSize", "too large"); err != nil { + t.Fatalf("run %d: FinishWithError: %v", i, err) + } + record := bytes.Index(w.response, []byte(`{"id":1}`)) + errMsg := bytes.Index(w.response, []byte("OverMaxRecordSize")) + if record < 0 || errMsg < 0 || record > errMsg { + t.Fatalf("run %d: record at %d, error at %d: queued record was dropped or reordered", i, record, errMsg) + } + } +} + +// TestMessageWriterFlushesBufferedAndQueuedRecordsBeforeError: one record has +// already been staged into the buffer and a second one is still queued when +// the error arrives. Both must precede the error, in order. +func TestMessageWriterFlushesBufferedAndQueuedRecordsBeforeError(t *testing.T) { + for i := range 100 { + w := &testResponseWriter{} + writer := newMessageWriter(w, nil) + if err := writer.SendRecord(newTestRecord(`{"id":1}` + "\n")); err != nil { + t.Fatal(err) + } + // Give the writer a chance to stage the first record; whether it + // did or not, the outcome must be the same. + if i%2 == 0 { + for range 100 { + if len(writer.payloadCh) == 0 { + break + } + } + } + if err := writer.SendRecord(newTestRecord(`{"id":2}` + "\n")); err != nil { + t.Fatal(err) + } + if err := writer.FinishWithError("OverMaxRecordSize", "too large"); err != nil { + t.Fatal(err) + } + if got := eventOrder(w.response, `{"id":1}`, `{"id":2}`, "OverMaxRecordSize"); !ascending(got) { + t.Fatalf("run %d: offsets %v: records must precede the error in order", i, got) + } + } +} + +// TestMessageWriterErrorWithoutRecords: an error with nothing queued writes +// only the error message, no empty Records event. +func TestMessageWriterErrorWithoutRecords(t *testing.T) { + w := &testResponseWriter{} + writer := newMessageWriter(w, nil) + if err := writer.FinishWithError("InternalError", "boom"); err != nil { + t.Fatal(err) + } + if !bytes.Contains(w.response, []byte("InternalError")) || bytes.Contains(w.response, []byte("Records")) { + t.Fatalf("unexpected response: %q", w.response) + } +} + +// TestMessageWriterStagesRecordsLargerThanTheBuffer: a record larger than the +// staging buffer is split across several Records events and nothing is lost +// or reordered, whether the stream ends with success or with an error. +func TestMessageWriterStagesRecordsLargerThanTheBuffer(t *testing.T) { + big := strings.Repeat("x", bufLength+bufLength/2) + want := "head\n" + big + "\n" + "tail\n" + for _, withError := range []bool{false, true} { + w := &testResponseWriter{} + writer := newMessageWriter(w, nil) + for _, rec := range []string{"head\n", big + "\n", "tail\n"} { + if err := writer.SendRecord(newTestRecord(rec)); err != nil { + t.Fatal(err) + } + } + if withError { + if err := writer.FinishWithError("OverMaxRecordSize", "too large"); err != nil { + t.Fatal(err) + } + } else if err := writer.Finish(10, 10); err != nil { + t.Fatal(err) + } + records, err := decodeEvents(t, w.response) + if string(records) != want { + t.Fatalf("withError=%v: got %d record bytes, want %d; head=%q", withError, len(records), len(want), string(records[:min(len(records), 8)])) + } + if withError && (err == nil || !strings.Contains(err.Error(), "OverMaxRecordSize")) { + t.Fatalf("expected OverMaxRecordSize after the records, got %v", err) + } + if !withError && err != nil { + t.Fatalf("unexpected error on the success path: %v", err) + } + } +} + +// TestMessageWriterSuccessOrder: the success path is unchanged: every record, +// then Stats, then End, and the client sees no error. +func TestMessageWriterSuccessOrder(t *testing.T) { + w := &testResponseWriter{} + writer := newMessageWriter(w, nil) + for _, rec := range []string{`{"id":1}`, `{"id":2}`} { + if err := writer.SendRecord(newTestRecord(rec + "\n")); err != nil { + t.Fatal(err) + } + } + if err := writer.Finish(20, 20); err != nil { + t.Fatal(err) + } + if got := eventOrder(w.response, `{"id":1}`, `{"id":2}`, "Stats", "End"); !ascending(got) { + t.Fatalf("offsets %v", got) + } + records, err := decodeEvents(t, w.response) + if err != nil || string(records) != "{\"id\":1}\n{\"id\":2}\n" { + t.Fatalf("records %q err %v", records, err) + } +} diff --git a/internal/s3select/select.go b/internal/s3select/select.go index 2bd7bab91..a7abcbeb0 100644 --- a/internal/s3select/select.go +++ b/internal/s3select/select.go @@ -39,7 +39,7 @@ import ( "github.com/minio/minio/internal/s3select/json" "github.com/minio/minio/internal/s3select/parquet" "github.com/minio/minio/internal/s3select/sql" - "github.com/minio/pkg/v3/env" + "github.com/pgsty/silo-pkg/v3/env" "github.com/pierrec/lz4/v4" ) diff --git a/update-credits.sh b/update-credits.sh index 1460a1d04..6382e7f09 100755 --- a/update-credits.sh +++ b/update-credits.sh @@ -1,212 +1,6 @@ #!/usr/bin/env bash -gocredits . >CREDITS +set -euo pipefail -echo "All community contributions are licensed under the terms of the Apache 2 license." >>CREDITS -echo "----------------------------------------------------------------" >>CREDITS -cat >>CREDITS <>CREDITS +# Keep the historical entry point aligned with make credits. +exec bash "$(dirname "${BASH_SOURCE[0]}")/buildscripts/gen-credits.sh" "$@"