docs(auth): preserve login runtime evidence
This commit is contained in:
@@ -0,0 +1,73 @@
|
||||
{
|
||||
"schema": "jarca-ledger.review-evidence/v1",
|
||||
"issue": 6,
|
||||
"pull_request": 7,
|
||||
"source_review": 51,
|
||||
"finding": "D6-23",
|
||||
"contract_sha256": "4f734169ec896747fe39fd7f3ab75d1fc0d8cdd4903045e665beba40d80eb1fe",
|
||||
"verified_at_utc": "2026-10-03T00:10:43Z",
|
||||
"source_revision": "e7242a89f7f1faacbe984713a5ad211bd7bcac2b",
|
||||
"source_tree_clean_before_evidence_write": true,
|
||||
"environment": {
|
||||
"os": "Linux 6.18.34+rpt-rpi-v8",
|
||||
"architecture": "aarch64",
|
||||
"python": "3.13.5",
|
||||
"starlette": "1.3.1",
|
||||
"httpx": "0.28.1",
|
||||
"streamlit": "1.58.0",
|
||||
"uvicorn": "0.49.0",
|
||||
"itsdangerous": "2.2.0",
|
||||
"credentials": "synthetic test-only values; not recorded",
|
||||
"signing_secret": "synthetic test-only value; not recorded"
|
||||
},
|
||||
"commands": [
|
||||
{
|
||||
"purpose": "Focused route, hardened cookie, event-loop offload, and failed-delay helper verification",
|
||||
"command": "uv run --extra dashboard --extra dev pytest -q tests/test_dashboard_auth_server.py tests/test_dashboard_authentication.py",
|
||||
"exit_code": 0,
|
||||
"result": "29 passed; one Starlette/httpx deprecation warning",
|
||||
"relevant_output": "............................. [100%]\n=============================== warnings summary ===============================\ntests/test_dashboard_auth_server.py:11\n /opt/data/gitea-agent/repositories/jsfamily/jarca-ledger-mcp/worktrees/issue-6/checkout/tests/test_dashboard_auth_server.py:11: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.\n from starlette.testclient import TestClient\n\n-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html"
|
||||
},
|
||||
{
|
||||
"purpose": "Real-delay concurrent failed login plus successful login and hardened cookie runtime probe",
|
||||
"command": "uv run --extra dashboard python - <<'PY'\nimport asyncio\nimport json\nimport os\nimport secrets\nimport time\n\nfrom starlette.requests import Request\n\nfrom dashboard import authentication\nfrom dashboard.auth_server import login\n\nadmin_id = \"synthetic-\" + secrets.token_hex(8)\nadmin_password = secrets.token_hex(16)\nos.environ[authentication.ADMIN_ID_ENV] = admin_id\nos.environ[authentication.ADMIN_PASSWORD_ENV] = admin_password\nos.environ[authentication.SIGNING_SECRET_ENV] = secrets.token_hex(32)\n\n\ndef request(password: str) -> Request:\n body = f\"admin_id={admin_id}&admin_password={password}\".encode()\n sent = False\n\n async def receive():\n nonlocal sent\n if sent:\n return {\"type\": \"http.disconnect\"}\n sent = True\n return {\"type\": \"http.request\", \"body\": body, \"more_body\": False}\n\n scope = {\n \"type\": \"http\",\n \"method\": \"POST\",\n \"scheme\": \"https\",\n \"path\": \"/api/auth/login\",\n \"raw_path\": b\"/api/auth/login\",\n \"query_string\": b\"\",\n \"headers\": [\n (b\"host\", b\"testserver\"),\n (b\"origin\", b\"https://testserver\"),\n (b\"content-type\", b\"application/x-www-form-urlencoded\"),\n ],\n \"client\": (\"testclient\", 50000),\n \"server\": (\"testserver\", 443),\n }\n return Request(scope, receive)\n\n\nasync def main() -> None:\n wrong_password = secrets.token_hex(16)\n started = time.monotonic()\n failures = [asyncio.create_task(login(request(wrong_password))) for _ in range(2)]\n await asyncio.sleep(0.1)\n probe_elapsed = time.monotonic() - started\n success_started = time.monotonic()\n success = await login(request(admin_password))\n success_elapsed = time.monotonic() - success_started\n failed = await asyncio.gather(*failures)\n total_elapsed = time.monotonic() - started\n cookie = success.headers.get(\"set-cookie\", \"\")\n assert success.status_code == 303\n assert success.headers[\"location\"] == \"/\"\n assert all(marker in cookie for marker in [\"HttpOnly\", \"Secure\", \"SameSite=lax\", \"Path=/\", \"Max-Age=86400\"])\n assert all(response.status_code == 303 for response in failed)\n assert all(\"set-cookie\" not in response.headers for response in failed)\n assert probe_elapsed < 0.5\n assert success_elapsed < 0.5\n assert 0.95 <= total_elapsed < 1.75\n print(json.dumps({\n \"success\": True,\n \"source_revision\": \"e7242a89f7f1faacbe984713a5ad211bd7bcac2b\",\n \"successful_login_status\": success.status_code,\n \"successful_login_cookie_hardened\": True,\n \"failed_login_statuses\": [response.status_code for response in failed],\n \"failed_login_cookie_absent\": True,\n \"event_loop_probe_elapsed_seconds\": round(probe_elapsed, 6),\n \"successful_login_elapsed_seconds\": round(success_elapsed, 6),\n \"concurrent_failed_logins_total_elapsed_seconds\": round(total_elapsed, 6),\n \"minimum_failed_login_delay_seconds\": 1.0,\n \"credentials_and_cookie_values_recorded\": False,\n }, indent=2))\n\n\nasyncio.run(main())\nPY",
|
||||
"exit_code": 0,
|
||||
"result": {
|
||||
"success": true,
|
||||
"source_revision": "e7242a89f7f1faacbe984713a5ad211bd7bcac2b",
|
||||
"successful_login_status": 303,
|
||||
"successful_login_cookie_hardened": true,
|
||||
"failed_login_statuses": [
|
||||
303,
|
||||
303
|
||||
],
|
||||
"failed_login_cookie_absent": true,
|
||||
"event_loop_probe_elapsed_seconds": 0.10056,
|
||||
"successful_login_elapsed_seconds": 0.001267,
|
||||
"concurrent_failed_logins_total_elapsed_seconds": 1.00614,
|
||||
"minimum_failed_login_delay_seconds": 1.0,
|
||||
"credentials_and_cookie_values_recorded": false
|
||||
},
|
||||
"relevant_output": "{\n \"success\": true,\n \"source_revision\": \"e7242a89f7f1faacbe984713a5ad211bd7bcac2b\",\n \"successful_login_status\": 303,\n \"successful_login_cookie_hardened\": true,\n \"failed_login_statuses\": [\n 303,\n 303\n ],\n \"failed_login_cookie_absent\": true,\n \"event_loop_probe_elapsed_seconds\": 0.10056,\n \"successful_login_elapsed_seconds\": 0.001267,\n \"concurrent_failed_logins_total_elapsed_seconds\": 1.00614,\n \"minimum_failed_login_delay_seconds\": 1.0,\n \"credentials_and_cookie_values_recorded\": false\n}"
|
||||
}
|
||||
],
|
||||
"hashes_sha256": {
|
||||
"dashboard/auth_server.py": "4b991fbe942b573f8be270538b7e9df81bca9ffed3a8982f09db0d95d26e66d1",
|
||||
"dashboard/authentication.py": "63df2e33ef10859039c9b8140e92cbf0c7c8c9fe2d19cfcedbf16637c8cc2596",
|
||||
"tests/test_dashboard_auth_server.py": "8b96421af776b613867a8a5ab5dd962033b39d1cc616ba4e91cf3c4de63a1f0d",
|
||||
"tests/test_dashboard_authentication.py": "1ca7ecc4d4db55c674aa2934d665b121476ff5fa2e1f74071090a67e815af676",
|
||||
"pyproject.toml": "64100d3aa1c2272df2e2139ed435724901f7215ea72e48bee9d73f95e7e53a8d"
|
||||
},
|
||||
"redaction": {
|
||||
"credentials_recorded": false,
|
||||
"signing_secret_recorded": false,
|
||||
"token_or_cookie_value_recorded": false,
|
||||
"request_body_recorded": false,
|
||||
"raw_set_cookie_recorded": false
|
||||
},
|
||||
"notes": [
|
||||
"This artifact is an evidence-only descendant of source_revision; the application and test paths hashed above were not modified before these commands ran.",
|
||||
"The runtime probe exercised the changed streaming login path with synthetic credentials, confirmed hardened 24-hour cookie issuance, retained the real one-second failed-login delay, and showed the event loop and a successful login remained responsive while two failed requests waited.",
|
||||
"This D6-23 evidence does not replace or claim a fresh browser run; D6-22 remains supported by docs/evidence/issue-6-persistent-auth-https.json and the Review #51 change-impact analysis."
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user