From ae3f581e3eedd2727ab54799a0937a870d22087b Mon Sep 17 00:00:00 2001 From: Jerome Tabiri Date: Mon, 8 Jun 2026 05:18:20 -0400 Subject: [PATCH] Phase 0.5: Re-add ADMIN_KEY, /admin/keys endpoints, dual-key logging to current HEAD MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Restores the Phase 0 security features that were lost during the merge of SyslogSolution/syslog-harness into abiba-bot/inference-harness (commit 815ed799). Changes: - Added ADMIN_KEY env var + _admin_auth() for endpoint protection - Added GET /admin/keys — list all keys with deprecation status - Added GET /admin/keys/deprecation-summary — deprecated key usage from Redis - Added POST /admin/keys/generate — generate new keys (memory-only) - Added POST /admin/keys/revoke — revoke deprecated keys (memory-only) - Added dual-key deprecated key logging to chat() endpoint - Preserves: Redis lazy reconnect, MoE spillover, 5-tier routing, vision guard --- router/router.py | 124 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 124 insertions(+) diff --git a/router/router.py b/router/router.py index 6fd9289..0919828 100644 --- a/router/router.py +++ b/router/router.py @@ -382,6 +382,17 @@ def chat(): ki = API_KEYS[ak] tier, agent = ki["tier"], ki["agent"] + # Phase 0: dual-key transition — log deprecated key usage + if ki.get("deprecated"): + new_key = next((k for k, v in API_KEYS.items() + if v.get("agent") == agent and not v.get("deprecated")), None) + log.warning("DEPRECATED_KEY: agent=%s using old key %s...%s — switch to %s...%s", + agent, ak[:12], ak[-8:], + new_key[:12] if new_key else "N/A", + new_key[-8:] if new_key else "N/A") + if r: + r.incr("deprecated_usage:" + agent) + # Rate limit check allowed, rl_val, reset_sec = check_rate_limit(ak, tier) if not allowed: @@ -740,6 +751,119 @@ def stream(): return Response(stream_with_context(ev()), mimetype="text/event-stream", headers={"Cache-Control":"no-cache","X-Accel-Buffering":"no","Access-Control-Allow-Origin":"*"}) +# ── Phase 0: Admin Key Management ── +ADMIN_KEY = os.environ.get("ADMIN_KEY", "") + +def _admin_auth(): + """Require admin key for management endpoints.""" + if not ADMIN_KEY: + return False, "ADMIN_KEY not configured on server" + ak = request.headers.get("Authorization","").replace("Bearer ","") + if ak != ADMIN_KEY: + return False, "Admin key required" + return True, None + +@app.route("/admin/keys") +def admin_keys(): + """List all API keys (masked) with agent, tier, and deprecation status.""" + ok, err = _admin_auth() + if not ok: return jsonify({"error": err}), 401 + keys = [] + for key, info in API_KEYS.items(): + masked = key[:8] + "..." + key[-8:] + keys.append({ + "masked": masked, + "prefix": key[:8], + "agent": info["agent"], + "tier": info["tier"], + "deprecated": info.get("deprecated", False), + "length": len(key) + }) + return jsonify({ + "total": len(keys), + "active": sum(1 for k in keys if not k["deprecated"]), + "deprecated": sum(1 for k in keys if k["deprecated"]), + "keys": sorted(keys, key=lambda k: (k["deprecated"], k["agent"])) + }) + +@app.route("/admin/keys/deprecation-summary") +def admin_deprecation_summary(): + """Summary of deprecated key usage (from Redis logs, if available).""" + ok, err = _admin_auth() + if not ok: return jsonify({"error": err}), 401 + deprecated_agents = [] + for key, info in API_KEYS.items(): + if info.get("deprecated"): + # Check Redis for usage count + count = 0 + if r: + count = int(r.get("deprecated_usage:" + info["agent"]) or 0) + deprecated_agents.append({ + "agent": info["agent"], + "deprecated_uses": count, + "needs_migration": count > 0 + }) + return jsonify({ + "deprecated_agents": sorted(deprecated_agents, key=lambda d: -d["deprecated_uses"]), + "recommendation": "Run POST /admin/keys/revoke to remove keys with 0 usage" + }) + +@app.route("/admin/keys/generate", methods=["POST"]) +def admin_generate_key(): + """Generate a new API key for an agent. Body: {"agent": "Name", "tier": "enterprise"}""" + ok, err = _admin_auth() + if not ok: return jsonify({"error": err}), 401 + body = request.get_json(force=True) + agent = body.get("agent", "").strip() + tier = body.get("tier", "enterprise") + if not agent: + return jsonify({"error": "agent field required"}), 400 + if tier not in ("starter", "professional", "enterprise"): + return jsonify({"error": "tier must be starter/professional/enterprise"}), 400 + # Generate secure key + import secrets, hashlib + prefix = hashlib.sha256(secrets.token_bytes(12)).hexdigest()[:8] + suffix = secrets.token_hex(20) + new_key = f"sk-{prefix}-{suffix}" + # Update in-memory dict (note: not persisted across restarts without env var update) + API_KEYS[new_key] = {"tier": tier, "agent": agent} + log.info("KEY_GENERATED: agent=%s tier=%s key=%s...%s", agent, tier, new_key[:8], new_key[-8:]) + return jsonify({ + "agent": agent, + "tier": tier, + "key": new_key, + "masked": new_key[:8] + "..." + new_key[-8:], + "warning": "This key exists in memory only. Update API_KEYS env var and redeploy to persist." + }), 201 + +@app.route("/admin/keys/revoke", methods=["POST"]) +def admin_revoke_key(): + """Revoke a deprecated key. Body: {"agent": "Name"} or {"key_prefix": "sk-xxxx"}""" + ok, err = _admin_auth() + if not ok: return jsonify({"error": err}), 401 + body = request.get_json(force=True) + agent = body.get("agent", "") + key_prefix = body.get("key_prefix", "") + revoked = [] + keys_to_remove = [] + for key, info in API_KEYS.items(): + if not info.get("deprecated"): + continue + if agent and info["agent"] == agent: + keys_to_remove.append(key) + elif key_prefix and key.startswith(key_prefix): + keys_to_remove.append(key) + for key in keys_to_remove: + info = API_KEYS.pop(key) + revoked.append({"agent": info["agent"], "masked": key[:8] + "..." + key[-8:]}) + log.warning("KEY_REVOKED: agent=%s key=%s...%s", info["agent"], key[:8], key[-8:]) + return jsonify({ + "revoked": len(revoked), + "keys": revoked, + "remaining_total": len(API_KEYS), + "warning": "Memory-only revoke. Update API_KEYS env var and redeploy to persist." + }) + if __name__ == "__main__": log.info("Router on :9000 (load-aware)") app.run(host="0.0.0.0", port=9000, debug=False)