add p1_secret_verify: mounted-credential end-to-end auth test
Browse files- probes/p1_secret_verify.py +129 -0
probes/p1_secret_verify.py
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Phase 1 verification: does the Kaggle-mounted secret store actually work end to end, and can a job
|
| 2 |
+
# write to the Hub with it?
|
| 3 |
+
#
|
| 4 |
+
# This is the decisive test for D-002. It carries NO token in its source: the credential arrives via
|
| 5 |
+
# the mounted private dataset dodosoomro/ounce100m-secret-store, which is the whole point of the store.
|
| 6 |
+
#
|
| 7 |
+
# Checks, in order:
|
| 8 |
+
# 1. the mount exists and parses,
|
| 9 |
+
# 2. token integrity -- length + a truncated sha256 compared against the value the bootstrap
|
| 10 |
+
# recorded, which proves the right credential without ever printing it,
|
| 11 |
+
# 3. HF read auth (whoami),
|
| 12 |
+
# 4. HF WRITE auth, the thing §3.13 needs: create a throwaway repo, commit a file, verify it is
|
| 13 |
+
# listed on the Hub from outside, delete it.
|
| 14 |
+
#
|
| 15 |
+
# Every optional diagnostic is wrapped: the bootstrap job died because one unwrapped cross-check
|
| 16 |
+
# raised AFTER the real work had succeeded, so the run produced no report at all (E-012). A probe that
|
| 17 |
+
# cannot survive its own instrumentation is not a probe.
|
| 18 |
+
#
|
| 19 |
+
# Zero GPU quota (CPU session).
|
| 20 |
+
|
| 21 |
+
import hashlib
|
| 22 |
+
import json
|
| 23 |
+
import os
|
| 24 |
+
import time
|
| 25 |
+
import urllib.error
|
| 26 |
+
import urllib.request
|
| 27 |
+
|
| 28 |
+
R = {}
|
| 29 |
+
MOUNT = "/kaggle/input/ounce100m-secret-store/credentials.json"
|
| 30 |
+
DS_ID = "dodosoomro/ounce100m-secret-store"
|
| 31 |
+
EXPECTED_SHA12 = "076ca223fcf6" # recorded by p1_secret_bootstrap v1, value-free
|
| 32 |
+
TEST_REPO = "Cion-lab/ounce100m-authtest-DELETEME"
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
def guard(name, fn):
|
| 36 |
+
try:
|
| 37 |
+
R[name] = fn()
|
| 38 |
+
except Exception as e:
|
| 39 |
+
R[name] = {"error": f"{type(e).__name__}: {e}"[:300]}
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
# ---- 1 + 2: the mount, checked without echoing anything secret
|
| 43 |
+
def read_mount():
|
| 44 |
+
if not os.path.exists(MOUNT):
|
| 45 |
+
siblings = sorted(os.listdir("/kaggle/input")) if os.path.isdir("/kaggle/input") else None
|
| 46 |
+
return {"mounted": False, "kaggle_input_listing": siblings,
|
| 47 |
+
"note": "the private dataset did not mount -- creation probably never succeeded"}
|
| 48 |
+
with open(MOUNT) as f:
|
| 49 |
+
blob = json.load(f)
|
| 50 |
+
tok = blob.get("HF_TOKEN", "")
|
| 51 |
+
return {"mounted": True, "keys": sorted(blob.keys()), "token_length": len(tok),
|
| 52 |
+
"token_sha256_prefix": hashlib.sha256(tok.encode()).hexdigest()[:12],
|
| 53 |
+
"matches_bootstrap_record": hashlib.sha256(tok.encode()).hexdigest()[:12] == EXPECTED_SHA12,
|
| 54 |
+
"mode": oct(os.stat(MOUNT).st_mode & 0o777)}
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
guard("mount", read_mount)
|
| 58 |
+
|
| 59 |
+
tok = ""
|
| 60 |
+
if isinstance(R.get("mount"), dict) and R["mount"].get("mounted"):
|
| 61 |
+
with open(MOUNT) as f:
|
| 62 |
+
tok = json.load(f).get("HF_TOKEN", "")
|
| 63 |
+
|
| 64 |
+
# ---- privacy, re-proved from the outside: an anonymous download must NOT work
|
| 65 |
+
def privacy():
|
| 66 |
+
url = f"https://www.kaggle.com/api/v1/datasets/download/{DS_ID}"
|
| 67 |
+
try:
|
| 68 |
+
with urllib.request.urlopen(url, timeout=60) as r:
|
| 69 |
+
return {"status": r.status, "verdict": "PUBLIC_LEAK"}
|
| 70 |
+
except urllib.error.HTTPError as e:
|
| 71 |
+
return {"status": e.code, "verdict": "PRIVATE" if e.code in (401, 403) else f"HTTP_{e.code}"}
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
guard("privacy_anonymous", privacy)
|
| 75 |
+
|
| 76 |
+
# ---- 3: HF read auth, using the mounted credential only in memory
|
| 77 |
+
def hf_read():
|
| 78 |
+
from huggingface_hub import HfApi
|
| 79 |
+
|
| 80 |
+
api = HfApi(token=tok)
|
| 81 |
+
me = api.whoami()
|
| 82 |
+
return {"name": me.get("name"), "type": me.get("type"),
|
| 83 |
+
"can_be_org": bool(me.get("orgs")), "token_kind": me.get("full", {}).get("fullname")}
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
guard("hf_whoami", hf_read)
|
| 87 |
+
|
| 88 |
+
# ---- 4: HF write auth -- the capability the checkpoint cycle depends on
|
| 89 |
+
def hf_write():
|
| 90 |
+
from huggingface_hub import HfApi, create_repo, delete_repo
|
| 91 |
+
|
| 92 |
+
api = HfApi(token=tok)
|
| 93 |
+
t0 = time.monotonic()
|
| 94 |
+
create_repo(TEST_REPO.split("/")[1], token=tok, repo_type="model",
|
| 95 |
+
namespace=TEST_REPO.split("/")[0], private=False, exist_ok=True)
|
| 96 |
+
api.upload_file(path_or_fileobj=b"ounce100m auth round-trip probe\n",
|
| 97 |
+
path_in_repo="authcheck.txt", repo_id=TEST_REPO, repo_type="model",
|
| 98 |
+
commit_message="auth write check")
|
| 99 |
+
# Independent verification: read it back over plain anonymous HTTPS, not through the client that
|
| 100 |
+
# wrote it, so a fake success in the library cannot be mistaken for a working upload path.
|
| 101 |
+
url = f"https://huggingface.co/{TEST_REPO}/resolve/main/authcheck.txt"
|
| 102 |
+
with urllib.request.urlopen(url, timeout=60) as r:
|
| 103 |
+
body = r.read()
|
| 104 |
+
verified = body.strip() == b"ounce100m auth round-trip probe"
|
| 105 |
+
delete_repo(TEST_REPO, repo_type="model", token=tok)
|
| 106 |
+
gone = False
|
| 107 |
+
try:
|
| 108 |
+
urllib.request.urlopen(url, timeout=30)
|
| 109 |
+
except urllib.error.HTTPError as e:
|
| 110 |
+
gone = e.code in (404, 401, 403)
|
| 111 |
+
except Exception:
|
| 112 |
+
pass
|
| 113 |
+
return {"created": True, "readback_verified": verified, "seconds": round(time.monotonic() - t0, 1),
|
| 114 |
+
"deleted": True, "delete_confirmed_gone": gone}
|
| 115 |
+
|
| 116 |
+
|
| 117 |
+
guard("hf_write_roundtrip", hf_write)
|
| 118 |
+
|
| 119 |
+
# ---- how a training job should use this, for the record
|
| 120 |
+
R["usage_pattern"] = {
|
| 121 |
+
"save_notebook_datasetDataSources": [DS_ID],
|
| 122 |
+
"read": f"json.load(open({MOUNT!r}))['HF_TOKEN']",
|
| 123 |
+
"rule": "set os.environ['HF_TOKEN'] in-process; never print the value; never copy the file into "
|
| 124 |
+
"/kaggle/working, because working-dir files become kernel output artifacts",
|
| 125 |
+
}
|
| 126 |
+
|
| 127 |
+
print("PROBE_JSON_BEGIN")
|
| 128 |
+
print(json.dumps(R, indent=1, default=str))
|
| 129 |
+
print("PROBE_JSON_END")
|