Phase 0.5: Re-add ADMIN_KEY, /admin/keys endpoints, dual-key logging to current HEAD
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
This commit is contained in:
@@ -382,6 +382,17 @@ def chat():
|
|||||||
ki = API_KEYS[ak]
|
ki = API_KEYS[ak]
|
||||||
tier, agent = ki["tier"], ki["agent"]
|
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
|
# Rate limit check
|
||||||
allowed, rl_val, reset_sec = check_rate_limit(ak, tier)
|
allowed, rl_val, reset_sec = check_rate_limit(ak, tier)
|
||||||
if not allowed:
|
if not allowed:
|
||||||
@@ -740,6 +751,119 @@ def stream():
|
|||||||
return Response(stream_with_context(ev()), mimetype="text/event-stream",
|
return Response(stream_with_context(ev()), mimetype="text/event-stream",
|
||||||
headers={"Cache-Control":"no-cache","X-Accel-Buffering":"no","Access-Control-Allow-Origin":"*"})
|
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__":
|
if __name__ == "__main__":
|
||||||
log.info("Router on :9000 (load-aware)")
|
log.info("Router on :9000 (load-aware)")
|
||||||
app.run(host="0.0.0.0", port=9000, debug=False)
|
app.run(host="0.0.0.0", port=9000, debug=False)
|
||||||
|
|||||||
Reference in New Issue
Block a user