petter2025 commited on
Commit
6fbc8ca
Β·
1 Parent(s): 84013a9

Upload folder using huggingface_hub

Browse files
app/api/routes_admin.py CHANGED
@@ -123,6 +123,46 @@ async def update_key_tier(
123
  return {"message": f"Tier updated to {req.tier}"}
124
 
125
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
126
  @router.delete("/keys/{key_id}", dependencies=[Depends(verify_admin)])
127
  async def deactivate_api_key(
128
  key_id: str = Path(..., description="The key_id from GET /admin/keys (not the raw API key)")):
 
123
  return {"message": f"Tier updated to {req.tier}"}
124
 
125
 
126
+ @router.post("/keys/{key_id}/rotate", dependencies=[Depends(verify_admin)])
127
+ async def rotate_api_key(
128
+ key_id: str = Path(..., description="The key_id from GET /admin/keys (not the raw API key)"),
129
+ ):
130
+ """Atomically deactivate a key and issue a new one on the same tenant
131
+ and tier -- the single action a leaked/compromised key actually needs.
132
+ Doing this as get-old + deactivate + create separately (the only option
133
+ before this endpoint existed) risks losing track of the tenant_id
134
+ partway through, or leaving the old key active if a later step fails.
135
+ The new plaintext key is returned exactly once, like create_api_key's."""
136
+ with tracker._get_pg_conn() as conn:
137
+ old_row = tracker._pg_execute(
138
+ conn, "SELECT tenant_id, tier FROM api_keys WHERE lookup_hash = %s", (key_id,)
139
+ ).fetchone()
140
+ if not old_row:
141
+ conn.rollback()
142
+ raise HTTPException(status_code=404, detail="API key not found")
143
+
144
+ new_key = f"sk_live_{uuid.uuid4().hex[:24]}"
145
+ salt = secrets.token_hex(16)
146
+ tracker._pg_execute(
147
+ conn, "UPDATE api_keys SET is_active = false WHERE lookup_hash = %s", (key_id,))
148
+ tracker._pg_execute(
149
+ conn,
150
+ "INSERT INTO api_keys "
151
+ "(tenant_id, tier, created_at, is_active, salt, key_hash, lookup_hash) "
152
+ "VALUES (%s, %s, %s, %s, %s, %s, %s)",
153
+ (old_row["tenant_id"], old_row["tier"], datetime.utcnow(), True,
154
+ salt, tracker._salted_hash(new_key, salt), tracker._lookup_hash(new_key)),
155
+ )
156
+ conn.commit()
157
+
158
+ return {
159
+ "api_key": new_key,
160
+ "tenant_id": old_row["tenant_id"],
161
+ "tier": old_row["tier"],
162
+ "deactivated_key_id": key_id,
163
+ }
164
+
165
+
166
  @router.delete("/keys/{key_id}", dependencies=[Depends(verify_admin)])
167
  async def deactivate_api_key(
168
  key_id: str = Path(..., description="The key_id from GET /admin/keys (not the raw API key)")):
app/api/routes_governance.py CHANGED
@@ -366,7 +366,7 @@ async def evaluate_intent_endpoint(
366
  span.set_status(Status(StatusCode.ERROR, error_msg))
367
  span.record_exception(e)
368
  span.end()
369
- raise HTTPException(status_code=500, detail=error_msg)
370
 
371
 
372
  # --------------------------------------------------------------------------
@@ -413,8 +413,9 @@ async def record_outcome_endpoint(
413
  logger.warning(f"Failed to update pricing buffer for intent {outcome.deterministic_id}: {e}")
414
 
415
  return {"message": "Outcome recorded", "outcome_id": outcome_record.id}
416
- except Exception as e:
417
- raise HTTPException(status_code=500, detail=str(e))
 
418
 
419
 
420
  # --------------------------------------------------------------------------
@@ -553,4 +554,4 @@ async def evaluate_healing_decision_endpoint(
553
  span.set_status(Status(StatusCode.ERROR, error_msg))
554
  span.record_exception(e)
555
  span.end()
556
- raise HTTPException(status_code=500, detail=error_msg)
 
366
  span.set_status(Status(StatusCode.ERROR, error_msg))
367
  span.record_exception(e)
368
  span.end()
369
+ raise HTTPException(status_code=500, detail="Internal server error")
370
 
371
 
372
  # --------------------------------------------------------------------------
 
413
  logger.warning(f"Failed to update pricing buffer for intent {outcome.deterministic_id}: {e}")
414
 
415
  return {"message": "Outcome recorded", "outcome_id": outcome_record.id}
416
+ except Exception:
417
+ logger.exception("Error recording outcome")
418
+ raise HTTPException(status_code=500, detail="Internal server error")
419
 
420
 
421
  # --------------------------------------------------------------------------
 
554
  span.set_status(Status(StatusCode.ERROR, error_msg))
555
  span.record_exception(e)
556
  span.end()
557
+ raise HTTPException(status_code=500, detail="Internal server error")
app/api/routes_history.py CHANGED
@@ -7,4 +7,4 @@ router = APIRouter(dependencies=[Depends(verify_internal_key)])
7
 
8
  @router.get("/history")
9
  async def get_history():
10
- return {"incidents": incident_history}
 
7
 
8
  @router.get("/history")
9
  async def get_history():
10
+ return {"incidents": list(incident_history)}
app/api/routes_incidents.py CHANGED
@@ -30,23 +30,20 @@ from agentic_reliability_framework.core.models.event import (
30
  ReliabilityEvent,
31
  )
32
 
 
33
  from app.causal_explainer import CausalExplainer
 
34
  from app.core.usage_tracker import UsageRecord, enforce_quota, tracker
35
 
36
  logger = logging.getLogger(__name__)
37
 
38
  router = APIRouter()
39
 
40
- # ---------------------------------------------------------------------------
41
- # In‑memory incident store (for auditing / debugging only)
42
- # ---------------------------------------------------------------------------
43
- incident_history: list[dict] = []
44
-
45
 
46
  # ---------------------------------------------------------------------------
47
  # POST /api/v1/report_incident
48
  # ---------------------------------------------------------------------------
49
- @router.post("/report_incident")
50
  async def report_incident(event: ReliabilityEvent) -> dict[str, str]:
51
  """
52
  Record a ``ReliabilityEvent`` in the in‑memory incident history.
@@ -54,7 +51,10 @@ async def report_incident(event: ReliabilityEvent) -> dict[str, str]:
54
  This endpoint is used by internal monitoring tools to feed incident
55
  data into the causal explainer and downstream analysis. The event
56
  is stored as a JSON‑safe dictionary and is **not** persisted across
57
- API restarts.
 
 
 
58
 
59
  Parameters
60
  ----------
@@ -249,6 +249,7 @@ async def evaluate_incident(
249
  raise
250
  except Exception as exc:
251
  error_msg = str(exc)
 
252
  if tracker:
253
  record = UsageRecord(
254
  api_key=api_key,
@@ -260,4 +261,4 @@ async def evaluate_incident(
260
  processing_ms=(time.time() - start_time) * 1000,
261
  )
262
  await tracker.increment_usage_async(record, background_tasks)
263
- raise HTTPException(status_code=500, detail=error_msg)
 
30
  ReliabilityEvent,
31
  )
32
 
33
+ from app.api.deps import verify_internal_key
34
  from app.causal_explainer import CausalExplainer
35
+ from app.core.storage import incident_history
36
  from app.core.usage_tracker import UsageRecord, enforce_quota, tracker
37
 
38
  logger = logging.getLogger(__name__)
39
 
40
  router = APIRouter()
41
 
 
 
 
 
 
42
 
43
  # ---------------------------------------------------------------------------
44
  # POST /api/v1/report_incident
45
  # ---------------------------------------------------------------------------
46
+ @router.post("/report_incident", dependencies=[Depends(verify_internal_key)])
47
  async def report_incident(event: ReliabilityEvent) -> dict[str, str]:
48
  """
49
  Record a ``ReliabilityEvent`` in the in‑memory incident history.
 
51
  This endpoint is used by internal monitoring tools to feed incident
52
  data into the causal explainer and downstream analysis. The event
53
  is stored as a JSON‑safe dictionary and is **not** persisted across
54
+ API restarts. Requires the same ``X-Internal-Key`` header every other
55
+ data-bearing route in this API requires -- previously this endpoint had
56
+ no auth dependency at all, so anyone could write into the incident
57
+ history that feeds the causal explainer and ``GET /history``.
58
 
59
  Parameters
60
  ----------
 
249
  raise
250
  except Exception as exc:
251
  error_msg = str(exc)
252
+ logger.exception("Error in evaluate_incident (deprecated endpoint)")
253
  if tracker:
254
  record = UsageRecord(
255
  api_key=api_key,
 
261
  processing_ms=(time.time() - start_time) * 1000,
262
  )
263
  await tracker.increment_usage_async(record, background_tasks)
264
+ raise HTTPException(status_code=500, detail="Internal server error")
app/api/routes_intents.py CHANGED
@@ -1,8 +1,12 @@
 
 
1
  from fastapi import APIRouter, Depends, HTTPException
2
  from app.api.deps import verify_internal_key
3
  from app.models.intent_models import IntentSimulation, IntentSimulationResponse
4
  from app.services.intent_service import simulate_intent
5
 
 
 
6
  router = APIRouter(dependencies=[Depends(verify_internal_key)])
7
 
8
 
@@ -11,5 +15,6 @@ async def simulate_intent_endpoint(intent: IntentSimulation):
11
  try:
12
  result = simulate_intent(intent)
13
  return IntentSimulationResponse(**result)
14
- except Exception as e:
15
- raise HTTPException(status_code=500, detail=str(e))
 
 
1
+ import logging
2
+
3
  from fastapi import APIRouter, Depends, HTTPException
4
  from app.api.deps import verify_internal_key
5
  from app.models.intent_models import IntentSimulation, IntentSimulationResponse
6
  from app.services.intent_service import simulate_intent
7
 
8
+ logger = logging.getLogger(__name__)
9
+
10
  router = APIRouter(dependencies=[Depends(verify_internal_key)])
11
 
12
 
 
15
  try:
16
  result = simulate_intent(intent)
17
  return IntentSimulationResponse(**result)
18
+ except Exception:
19
+ logger.exception("simulate_intent failed")
20
+ raise HTTPException(status_code=500, detail="Internal server error")
app/api/routes_payments.py CHANGED
@@ -2,6 +2,7 @@
2
  Payment endpoints – Stripe Checkout integration.
3
  """
4
 
 
5
  import os
6
  import stripe
7
  from fastapi import APIRouter, HTTPException
@@ -9,6 +10,8 @@ from pydantic import BaseModel
9
 
10
  from app.core.usage_tracker import tracker, Tier
11
 
 
 
12
  router = APIRouter(prefix="/payments", tags=["payments"])
13
 
14
  # Set Stripe API key (from environment)
@@ -59,5 +62,6 @@ async def create_checkout_session(req: CheckoutRequest):
59
  subscription_data={"metadata": {"api_key": req.api_key}},
60
  )
61
  return {"sessionId": checkout_session.id, "url": checkout_session.url}
62
- except Exception as e:
63
- raise HTTPException(status_code=500, detail=str(e))
 
 
2
  Payment endpoints – Stripe Checkout integration.
3
  """
4
 
5
+ import logging
6
  import os
7
  import stripe
8
  from fastapi import APIRouter, HTTPException
 
10
 
11
  from app.core.usage_tracker import tracker, Tier
12
 
13
+ logger = logging.getLogger(__name__)
14
+
15
  router = APIRouter(prefix="/payments", tags=["payments"])
16
 
17
  # Set Stripe API key (from environment)
 
62
  subscription_data={"metadata": {"api_key": req.api_key}},
63
  )
64
  return {"sessionId": checkout_session.id, "url": checkout_session.url}
65
+ except Exception:
66
+ logger.exception("create_checkout_session failed")
67
+ raise HTTPException(status_code=500, detail="Internal server error")
app/api/routes_risk.py CHANGED
@@ -1,8 +1,12 @@
 
 
1
  from fastapi import APIRouter, Depends, HTTPException
2
  from app.api.deps import verify_internal_key
3
  from app.models.risk_models import RiskResponse
4
  from app.services.risk_service import get_system_risk
5
 
 
 
6
  router = APIRouter(dependencies=[Depends(verify_internal_key)])
7
 
8
 
@@ -14,8 +18,9 @@ async def get_risk():
14
  raise HTTPException(
15
  status_code=501,
16
  detail="This endpoint is deprecated and not implemented")
17
- except Exception as e:
18
- raise HTTPException(status_code=500, detail=str(e))
 
19
 
20
  if risk < 0.3:
21
  status = "low"
 
1
+ import logging
2
+
3
  from fastapi import APIRouter, Depends, HTTPException
4
  from app.api.deps import verify_internal_key
5
  from app.models.risk_models import RiskResponse
6
  from app.services.risk_service import get_system_risk
7
 
8
+ logger = logging.getLogger(__name__)
9
+
10
  router = APIRouter(dependencies=[Depends(verify_internal_key)])
11
 
12
 
 
18
  raise HTTPException(
19
  status_code=501,
20
  detail="This endpoint is deprecated and not implemented")
21
+ except Exception:
22
+ logger.exception("get_risk failed")
23
+ raise HTTPException(status_code=500, detail="Internal server error")
24
 
25
  if risk < 0.3:
26
  status = "low"
app/core/storage.py CHANGED
@@ -1,2 +1,16 @@
1
- # Simple in-memory list for incident history
2
- incident_history = []
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """In-memory store for recent incident reports.
2
+
3
+ Bounded (maxlen), not persisted across restarts -- exists to give the
4
+ causal explainer and GET /history recent context, not as a durable audit
5
+ trail. The cap protects against unbounded memory growth from
6
+ POST /report_incident, which can be called repeatedly by anything holding
7
+ a valid internal key.
8
+
9
+ Shared by app.api.routes_incidents (writes, via report_incident) and
10
+ app.api.routes_history (reads, via GET /history) -- both must import this
11
+ same object rather than declaring their own list, or writes and reads
12
+ silently operate on two different lists.
13
+ """
14
+ from collections import deque
15
+
16
+ incident_history: deque = deque(maxlen=10_000)
app/main.py CHANGED
@@ -35,8 +35,9 @@ import time as _time
35
  from contextlib import asynccontextmanager
36
  from typing import Dict
37
 
38
- from fastapi import FastAPI
39
  from fastapi.middleware.cors import CORSMiddleware
 
40
 
41
  # ── Optional: Prometheus metrics ─────────────────────────────
42
  try:
@@ -371,6 +372,19 @@ def create_app() -> FastAPI:
371
  )
372
  logger.debug("CORS middleware configured")
373
 
 
 
 
 
 
 
 
 
 
 
 
 
 
374
  # ── Rate limiter ──────────────────────────────────────────
375
  if SLOWAPI_AVAILABLE:
376
  app.state.limiter = limiter
 
35
  from contextlib import asynccontextmanager
36
  from typing import Dict
37
 
38
+ from fastapi import FastAPI, Request
39
  from fastapi.middleware.cors import CORSMiddleware
40
+ from fastapi.responses import JSONResponse
41
 
42
  # ── Optional: Prometheus metrics ─────────────────────────────
43
  try:
 
372
  )
373
  logger.debug("CORS middleware configured")
374
 
375
+ # ── Generic exception handler ────────────────────────────
376
+ # Defense-in-depth for anything that escapes a route's own try/except
377
+ # uncaught (HTTPException instances are unaffected -- FastAPI's own,
378
+ # more specific handler for those still takes precedence). Logs the
379
+ # real exception server-side and returns a generic message: routes
380
+ # that catch their own exceptions have each been fixed to do the same,
381
+ # but this exists so a bug that skips that pattern doesn't leak raw
382
+ # exception text (paths, internals, third-party SDK details) to callers.
383
+ @app.exception_handler(Exception)
384
+ async def unhandled_exception_handler(request: Request, exc: Exception):
385
+ logger.exception("Unhandled exception on %s %s", request.method, request.url.path)
386
+ return JSONResponse(status_code=500, content={"detail": "Internal server error"})
387
+
388
  # ── Rate limiter ──────────────────────────────────────────
389
  if SLOWAPI_AVAILABLE:
390
  app.state.limiter = limiter
tests/test_routes_admin.py CHANGED
@@ -81,3 +81,37 @@ def test_update_nonexistent_key_returns_404(client):
81
  json={"tier": "pro"},
82
  )
83
  assert resp.status_code == 404
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
81
  json={"tier": "pro"},
82
  )
83
  assert resp.status_code == 404
84
+
85
+
86
+ def test_rotate_key_deactivates_old_and_creates_new_on_same_tenant(client):
87
+ create_resp = client.post(
88
+ "/admin/keys",
89
+ params={"admin_key": TEST_ADMIN_KEY},
90
+ json={"tier": "pro", "org_name": "Rotate Test Org"},
91
+ )
92
+ assert create_resp.status_code == 200
93
+ old_body = create_resp.json()
94
+ old_key_id = _key_id(old_body["api_key"])
95
+ tenant_id = old_body["tenant_id"]
96
+
97
+ rotate_resp = client.post(
98
+ f"/admin/keys/{old_key_id}/rotate", params={"admin_key": TEST_ADMIN_KEY})
99
+ assert rotate_resp.status_code == 200
100
+ rotated = rotate_resp.json()
101
+ assert rotated["tenant_id"] == tenant_id
102
+ assert rotated["tier"] == "pro"
103
+ assert rotated["deactivated_key_id"] == old_key_id
104
+ new_key_id = _key_id(rotated["api_key"])
105
+ assert new_key_id != old_key_id
106
+
107
+ list_resp = client.get("/admin/keys", params={"admin_key": TEST_ADMIN_KEY})
108
+ keys_by_id = {row["key_id"]: row for row in list_resp.json()["keys"]}
109
+ assert keys_by_id[old_key_id]["is_active"] is False
110
+ assert keys_by_id[new_key_id]["is_active"] is True
111
+ assert keys_by_id[new_key_id]["tier"] == "pro"
112
+
113
+
114
+ def test_rotate_nonexistent_key_returns_404(client):
115
+ resp = client.post(
116
+ "/admin/keys/does-not-exist/rotate", params={"admin_key": TEST_ADMIN_KEY})
117
+ assert resp.status_code == 404