mirror of
https://github.com/pgsty/minio.git
synced 2026-09-12 05:24:03 +03:00
48e1846525
Remove the eight explicit curve overrides so Go 1.27 honors tlsmlkem=0 across Server listeners, node links and outbound transports. Remove the unused shared curve option and add wire-level regression coverage. Document CA trust and TLS upgrade behavior, retain the investigation artifacts, and exclude their synthetic routes from the rebrand guard. The product compatibility baseline remains unchanged. Validation: focused race tests, HTTP tests, lint, compatibility guard positive/negative controls, and a fresh Linux build with three isolated OIDC integration scenarios all pass. Adversarial review: Claude Code Fable 5.1, max effort. Final verdict: APPROVE FOR COMMIT. Signed-off-by: Feng Ruohang <rh@vonng.com>
171 lines
8.8 KiB
Python
171 lines
8.8 KiB
Python
#!/usr/bin/env python3
|
|
"""Bounded, loopback-only full-Server comparison. See the investigation report.
|
|
|
|
Run in an isolated generic Linux container with locally built binaries in
|
|
/lab/bin and a new disposable /lab/out. No customer identities or endpoints.
|
|
"""
|
|
import http.cookiejar
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
import secrets
|
|
import shutil
|
|
import socket
|
|
import ssl
|
|
import subprocess
|
|
import time
|
|
import urllib.error
|
|
import urllib.parse
|
|
import urllib.request
|
|
|
|
ROOT = Path("/lab")
|
|
OUT = ROOT / "out"
|
|
BASE = {k: v for k, v in os.environ.items()
|
|
if not k.startswith(("MINIO_", "SILO_", "CONSOLE_"))
|
|
and k.lower() not in {"http_proxy", "https_proxy", "all_proxy", "no_proxy", "godebug"}}
|
|
|
|
|
|
def port():
|
|
with socket.socket() as sock:
|
|
sock.bind(("127.0.0.1", 0))
|
|
return sock.getsockname()[1]
|
|
|
|
|
|
def request(url, opener=None, payload=None):
|
|
headers = {"Origin": f"http://{urllib.parse.urlparse(url).netloc}"}
|
|
if payload is not None:
|
|
headers["Content-Type"] = "application/json"
|
|
req = urllib.request.Request(url, data=None if payload is None else json.dumps(payload).encode(), headers=headers)
|
|
try:
|
|
response = (opener.open if opener else urllib.request.urlopen)(req, timeout=2)
|
|
except urllib.error.HTTPError as err:
|
|
response = err
|
|
except (urllib.error.URLError, TimeoutError):
|
|
return 0, {}, b""
|
|
with response:
|
|
return response.code, dict(response.headers), response.read(1 << 20)
|
|
|
|
|
|
def stop(proc):
|
|
proc.terminate()
|
|
try:
|
|
proc.wait(timeout=4)
|
|
except subprocess.TimeoutExpired:
|
|
proc.kill()
|
|
proc.wait(timeout=2)
|
|
|
|
|
|
class NoRedirect(urllib.request.HTTPRedirectHandler):
|
|
def redirect_request(self, req, fp, code, msg, headers, newurl):
|
|
return None
|
|
|
|
|
|
def login(console, ca):
|
|
jar = http.cookiejar.CookieJar()
|
|
client = urllib.request.build_opener(urllib.request.HTTPCookieProcessor(jar))
|
|
status, _, raw = request(console + "/api/v1/login", client)
|
|
details = json.loads(raw)
|
|
rules = details.get("redirectRules", [])
|
|
assert status == 200 and len(rules) == 1, (status, details)
|
|
auth_url = rules[0]["redirect"]
|
|
provider = urllib.request.build_opener(NoRedirect(), urllib.request.HTTPSHandler(context=ssl.create_default_context(cafile=str(ca))))
|
|
status, headers, _ = request(auth_url, provider)
|
|
callback = headers.get("Location", headers.get("location", ""))
|
|
assert status == 302 and callback.startswith(console + "/oauth_callback?"), (status, callback)
|
|
values = urllib.parse.parse_qs(urllib.parse.urlparse(callback).query)
|
|
callback_status, _, _ = request(callback, client)
|
|
status, _, _ = request(console + "/api/v1/login/oauth2/auth", client,
|
|
{"code": values["code"][0], "state": values["state"][0]})
|
|
buckets_status, _, _ = request(console + "/api/v1/buckets", client)
|
|
# Do not record cookies, codes, JWTs, or the state value.
|
|
return {"callback_status": callback_status, "login_status": status,
|
|
"buckets_status": buckets_status, "session_cookie": any(c.name == "token" for c in jar)}
|
|
|
|
|
|
def run(name, binary, mode="normal", debug=None, tls13=False,
|
|
expected=True, trusted=True, oidc=True, oauth=False, add=False):
|
|
d = OUT / name
|
|
(d / "certs/CAs").mkdir(parents=True, exist_ok=False)
|
|
idp = d / "idp"
|
|
idp.mkdir()
|
|
(idp / "mode").write_text(mode)
|
|
with (d / "fixture.jsonl").open("w") as events, (d / "fixture.stderr").open("w") as errors, (d / "server.log").open("w") as logs:
|
|
fixture = subprocess.Popen([str(ROOT / "bin/fixture"), "-dir", str(idp), *(["-tls13"] if tls13 else [])], env=BASE, stdout=events, stderr=errors)
|
|
server = None
|
|
try:
|
|
until = time.monotonic() + 5
|
|
while not (idp / "url").is_file() and time.monotonic() < until:
|
|
time.sleep(.05)
|
|
assert (idp / "url").is_file(), "fixture did not initialize"
|
|
url = (idp / "url").read_text() + "/.well-known/openid-configuration"
|
|
if trusted:
|
|
shutil.copyfile(idp / "ca.pem", d / "certs/CAs/lab.pem")
|
|
sport, cport = port(), port()
|
|
address = f"127.0.0.1:{sport}"
|
|
api, console = "http://" + address, f"http://127.0.0.1:{cport}"
|
|
password = secrets.token_urlsafe(24)
|
|
env = dict(BASE, MINIO_ROOT_USER="local154", MINIO_ROOT_PASSWORD=password, MINIO_BROWSER="on")
|
|
if debug:
|
|
env["GODEBUG"] = debug
|
|
if oidc:
|
|
env.update(MINIO_IDENTITY_OPENID_CONFIG_URL=url,
|
|
MINIO_IDENTITY_OPENID_CLIENT_ID="local154",
|
|
MINIO_IDENTITY_OPENID_CLIENT_SECRET="local154-placeholder",
|
|
MINIO_IDENTITY_OPENID_REDIRECT_URI=console + "/oauth_callback")
|
|
server = subprocess.Popen([str(ROOT / "bin" / binary), "--config-dir", str(d / "config"), "--certs-dir", str(d / "certs"), "server", "--address", address, "--console-address", f"127.0.0.1:{cport}", str(d / "data")], env=env, stdout=logs, stderr=subprocess.STDOUT)
|
|
until = time.monotonic() + 15
|
|
while time.monotonic() < until:
|
|
assert server.poll() is None, "Server exited; inspect its local log"
|
|
status, _, _ = request(api + "/minio/health/cluster")
|
|
if status == 200 and request(console)[0] == 200:
|
|
break
|
|
if not expected and (d / "server.log").read_text().count("Waiting for OpenID") >= 2:
|
|
break
|
|
time.sleep(.1)
|
|
result = {"case": name, "binary": binary, "mode": mode, "godebug": debug, "tls13": tls13,
|
|
"cluster": request(api + "/minio/health/cluster")[0],
|
|
"ready": request(api + "/minio/health/ready")[0], "console": request(console)[0]}
|
|
assert result["cluster"] == (200 if expected else 503), result
|
|
aenv = dict(BASE, LAB_SERVER=address, LAB_USER="local154", LAB_PASSWORD=password, LAB_OIDC_URL=url)
|
|
if expected:
|
|
admin = subprocess.run([str(ROOT / "bin/admin-check")], env=aenv, capture_output=True, text=True, timeout=7)
|
|
result["admin_list_ok"] = admin.returncode == 0
|
|
assert result["admin_list_ok"], admin.stdout
|
|
curl = subprocess.run(["curl", "--cacert", str(idp / "ca.pem"), "--http2", "--max-time", "3", "-sS", "-o", "/dev/null", "-w", "%{http_code} %{http_version}", url], env=BASE, capture_output=True, text=True, timeout=5)
|
|
result["curl"] = {"exit": curl.returncode, "status_protocol": curl.stdout}
|
|
if add:
|
|
attempt = subprocess.run([str(ROOT / "bin/admin-check"), "add"], env=aenv, capture_output=True, text=True, timeout=7)
|
|
result["add_ok"] = attempt.returncode == 0
|
|
result["add_reset"] = "connection reset by peer" in attempt.stdout
|
|
assert result["add_ok"] == binary.startswith("candidate"), result
|
|
if oauth:
|
|
result["oauth"] = login(console, idp / "ca.pem")
|
|
assert result["oauth"]["login_status"] == 204 and result["oauth"]["buckets_status"] == 200, result
|
|
for bad in ("bad-signature", "bad-audience"):
|
|
(idp / "mode").write_text(bad)
|
|
result[bad] = login(console, idp / "ca.pem")
|
|
assert result[bad]["login_status"] >= 400 and result[bad]["buckets_status"] >= 400, result
|
|
# Public handshake metadata only; no authorization parameters.
|
|
result["events"] = [json.loads(line) for line in (d / "fixture.jsonl").read_text().splitlines()]
|
|
(d / "result.json").write_text(json.dumps(result, indent=2) + "\n")
|
|
print(json.dumps({k: v for k, v in result.items() if k != "events"}), flush=True)
|
|
finally:
|
|
if server is not None:
|
|
stop(server)
|
|
stop(fixture)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
run("old126-normal", "old-go126")
|
|
run("old127-normal", "old-go127")
|
|
run("old126-compat", "old-go126", "reject-mlkem", "tlsmlkem=0")
|
|
run("old127-compat", "old-go127", "reject-mlkem", "tlsmlkem=0", expected=False)
|
|
run("head127-compat", "head-go127", "reject-mlkem", "tlsmlkem=0", expected=False)
|
|
run("candidate127-compat-login", "candidate-go127", "reject-mlkem", "tlsmlkem=0", oauth=True)
|
|
run("candidate127-no-optout", "candidate-go127", "reject-mlkem", expected=False)
|
|
run("candidate127-tls13-login", "candidate-go127", tls13=True, oauth=True)
|
|
run("candidate127-untrusted", "candidate-go127", trusted=False, expected=False)
|
|
run("candidate127-mldsa", "candidate-go127", "reject-mldsa", "tlsmlkem=0", expected=False)
|
|
run("head127-add", "head-go127", "reject-mlkem", "tlsmlkem=0", oidc=False, add=True)
|
|
run("candidate127-add", "candidate-go127", "reject-mlkem", "tlsmlkem=0", oidc=False, add=True)
|