Skip to content

Harden per-bucket CORS replication, recovery, and compatibility after #71 #75

Description

@Vonng

Context

PR #71 adds the standard S3 per-bucket CORS control plane and enforcement path. The core single-site implementation has been accepted: PutBucketCors, GetBucketCors, and DeleteBucketCors work with a standard minio-go client; matching and rejected preflights behave as expected; configured response headers are applied; and buckets without a CORS configuration retain the existing global fallback.

The maintainer is accepting the contribution and taking ownership of the remaining integration and compatibility hardening in this issue. This issue is a release gate for shipping per-bucket CORS; merging #71 does not by itself make the feature release-ready.

CORS is a browser response policy, not an IAM authorization mechanism. The replication bugs below do not bypass S3 permissions, but they can leave an origin enabled on one site after an administrator intended to revoke it.

Release-blocking correctness work

1. Preserve CORS tombstones and source ordering across site replication

The current peer handler and heal path do not preserve last-writer-wins semantics across PUT and DELETE:

  • SiteReplicationMetaInfo only exports CorsConfigUpdatedAt when CorsConfigXML is non-empty. A DELETE therefore loses its tombstone timestamp in the status/heal snapshot.
  • PeerBucketCorsConfigHandler checks staleness through GetCorsConfig. Once the local config is deleted, that getter returns not-found and the newer deletion timestamp is unavailable, so an older PUT can resurrect the config.
  • Peer PUT/DELETE calls use BucketMetadataSys.Update/Delete, which generate a new local timestamp instead of persisting the source UpdatedAt. Delayed or reordered events can therefore be accepted or rejected using arrival time rather than source order.
  • When one site misses a DELETE, heal can currently select the remaining old non-nil config as the newest state and restore it to the site where it was deleted.

Required behavior:

  • Default CorsConfigUpdatedAt consistently for metadata that predates this field.
  • Export CorsConfigUpdatedAt even when CorsConfig is nil, so a deletion remains an observable tombstone.
  • Compare incoming events against the raw stored CORS timestamp regardless of whether a live config exists.
  • Persist the incoming source UpdatedAt for both PUT and DELETE instead of replacing it with local arrival time.
  • Make duplicate, delayed, and reordered CORS PUT/DELETE events idempotent and last-writer-wins.
  • Make heal choose and propagate a newer nil tombstone over an older non-nil config.

2. Add tests against the real apply/status/heal seams

The tests added in #71 cover JSON/base64 transport and pointer equality, but not metadata application or convergence. Add regression coverage for:

  • A newer peer DELETE is applied after a delayed older peer PUT.
  • An older peer PUT cannot resurrect a config after a newer local or peer DELETE.
  • A CORS deletion timestamp survives SiteReplicationMetaInfo with CorsConfig == nil.
  • Heal removes a stale peer config when the newest state is a tombstone.
  • Normal peer PUT, replacement PUT, and DELETE preserve the exact source timestamps.
  • Initial site sync and retry are idempotent.
  • At least one multi-site or faithful peer-handler integration test exercises the full dispatch-to-metadata path.

3. Do not fail open on metadata errors

The middleware should use the global CORS fallback only when the bucket genuinely has no per-bucket configuration. A parse, cache, or operational metadata error must not silently widen the bucket to the global policy.

  • Distinguish no CORS config from metadata/parse failures.
  • Define and test the safe response behavior for a configured bucket whose CORS metadata cannot be read.

Compatibility and operational cleanup

4. Complete the S3 preflight response

  • Return configured Access-Control-Expose-Headers on a successful OPTIONS response.
  • Keep Vary: Origin, Access-Control-Request-Method, Access-Control-Request-Headers coverage.
  • Add response-level tests for allowed headers, exposed headers, max age, and cache variation.

5. Correct site-replication status accounting

TotalCorsConfigCount currently uses the cumulative corsCfgCount > 0 condition while iterating sites. Once an earlier site has a config, a later site without one can be counted as configured.

  • Increment each site's total from that site's s.CorsConfig != nil state.
  • Add asymmetric-site tests: config on first site only, last site only, all sites, and no sites.
  • Audit the neighboring tag/SSE/version counters for the same inherited cumulative-count pattern; fix them in a separate focused commit or open dedicated issues if the scope is broader.

6. Finish validation edge cases

  • Reject empty AllowedOrigin values rather than only checking that an element exists.
  • Decide and test whether methods must use the canonical uppercase S3 values.
  • Ensure only the documented * wildcard semantics are accepted; do not inherit ? wildcard behavior from a generic matcher.
  • Treat the 255-character rule ID limit as characters rather than bytes, or document the deliberate byte limit.
  • Add malformed checksum, missing checksum, oversized XML, trailing XML, and invalid-rule HTTP tests.

7. Document upgrade, fallback, and rollback semantics accurately

CORS data is stored in the bucket metadata record under the logical cors.xml selector; it is not an independent dormant file that is guaranteed to survive all downgrade writes.

  • Document that an older binary does not enforce the new config.
  • Document that an older binary rewriting bucket metadata may drop unknown CORS fields.
  • Document that a bucket-level config overrides, rather than merges with, the global CORS policy; DELETE restores the global fallback.
  • Add/update public operator documentation in pgsty/silo.pgsty.com as a separate repository deliverable.

Inherited site-replication pattern audit

The CORS implementation mirrors existing policy/tag/SSE/quota handlers. Some of the timestamp and cumulative-counter behavior may therefore predate #71.

  • Audit existing bucket-metadata peer handlers for the same tombstone/source-timestamp problem.
  • Prefer a small shared source-timestamp-preserving helper if it reduces duplicated correctness logic without broad refactoring.
  • If other metadata types are demonstrably affected, open focused issues with reproductions instead of silently expanding this change without bounds.

Verification and release gate

  • go build ./...
  • Focused CORS and site-replication tests
  • Focused race tests
  • Full cmd package tests
  • golangci-lint with the repository-pinned version
  • Generated-file and compatibility checks
  • Standard minio-go end-to-end PUT/GET/DELETE and preflight smoke test
  • GitHub CI green on the follow-up PR and on main
  • No release tag or Docker publication containing feat: per-bucket CORS configuration (S3 ?cors) with enforcement #71 until the release-blocking items above are complete

Out of scope

  • Silo Console UI for editing bucket CORS is a separate follow-up.
  • A broad redesign of site replication is not required unless the audit produces a concrete shared bug and a narrowly reviewable fix.

Acceptance criteria

This issue can close when per-bucket CORS converges correctly across normal delivery, retry, reordering, missed DELETE, and heal; errors cannot widen a configured bucket to the global policy; the protocol/status cleanup is tested; documentation describes the operational boundary; and all local and remote release gates are green.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    Status
    Done

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions