Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 21 additions & 9 deletions api/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,8 @@ def __exit__(self, *a): pass
ModelCategory, Assistant, CreateAssistantRequest, AssistantChatRequest
)
from store import init_store, get_store
from security_utils import safe_url_segment, validate_outbound_url, validate_sql_identifier # lgtm[py/unused-import]
from urllib.parse import quote as _urlquote
from security_utils import safe_agent_segment, safe_url_segment, validate_outbound_url, validate_sql_identifier # lgtm[py/unused-import]
from urllib.parse import quote as _urlquote # noqa: F401 — kept for downstream callers

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -1140,8 +1140,11 @@ async def agent_session_approvals(session_id: str, request: Request):
if not _INTERNAL_API_TOKEN:
raise HTTPException(status_code=503, detail="agent: INTERNAL_API_TOKEN not configured")
user = _caller_id(request)
safe_user = _urlquote(user, safe="")
safe_sid = _urlquote(session_id, safe="")
try:
safe_user = safe_agent_segment(user)
safe_sid = safe_agent_segment(session_id)
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
async with httpx.AsyncClient(timeout=httpx.Timeout(15.0)) as client:
r = await client.get(f"{_AGENT_URL}/v1/sessions/{safe_user}/{safe_sid}/approvals", headers=_agent_headers(request))
return JSONResponse(status_code=r.status_code, content=r.json())
Expand All @@ -1166,7 +1169,10 @@ async def agent_sessions_list(request: Request):
if not _INTERNAL_API_TOKEN:
raise HTTPException(status_code=503, detail="agent: INTERNAL_API_TOKEN not configured")
user = _caller_id(request)
safe_user = _urlquote(user, safe="")
try:
safe_user = safe_agent_segment(user)
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
async with httpx.AsyncClient(timeout=httpx.Timeout(15.0)) as client:
r = await client.get(f"{_AGENT_URL}/v1/sessions/{safe_user}", headers=_agent_headers(request))
return JSONResponse(status_code=r.status_code, content=r.json())
Expand All @@ -1177,8 +1183,11 @@ async def agent_session_get(session_id: str, request: Request):
if not _INTERNAL_API_TOKEN:
raise HTTPException(status_code=503, detail="agent: INTERNAL_API_TOKEN not configured")
user = _caller_id(request)
safe_user = _urlquote(user, safe="")
safe_sid = _urlquote(session_id, safe="")
try:
safe_user = safe_agent_segment(user)
safe_sid = safe_agent_segment(session_id)
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
async with httpx.AsyncClient(timeout=httpx.Timeout(15.0)) as client:
r = await client.get(f"{_AGENT_URL}/v1/sessions/{safe_user}/{safe_sid}", headers=_agent_headers(request))
return JSONResponse(status_code=r.status_code, content=r.json())
Expand All @@ -1189,8 +1198,11 @@ async def agent_session_delete(session_id: str, request: Request):
if not _INTERNAL_API_TOKEN:
raise HTTPException(status_code=503, detail="agent: INTERNAL_API_TOKEN not configured")
user = _caller_id(request)
safe_user = _urlquote(user, safe="")
safe_sid = _urlquote(session_id, safe="")
try:
safe_user = safe_agent_segment(user)
safe_sid = safe_agent_segment(session_id)
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
async with httpx.AsyncClient(timeout=httpx.Timeout(15.0)) as client:
r = await client.delete(f"{_AGENT_URL}/v1/sessions/{safe_user}/{safe_sid}", headers=_agent_headers(request))
return JSONResponse(status_code=r.status_code, content=r.json())
Expand Down
20 changes: 20 additions & 0 deletions api/security_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,26 @@ def validate_sql_identifier(name: str, *, allow_dot: bool = False) -> str:
# pip-*, trp-*, asst-*) plus user-defined transform/pipeline names.
_SEGMENT_RE = re.compile(r"^[A-Za-z0-9_.\-]{1,128}$")

# Caller-id / agent-session segments — also allow '@' and '+' for UPNs / emails.
_AGENT_SEGMENT_RE = re.compile(r"^[A-Za-z0-9_.+@\-]{1,256}$")


def safe_agent_segment(value: str) -> str:
"""Validate a caller-id or session-id for use as a URL path segment.

Accepts the union of IDs, GUIDs, emails and UPNs. Rejects anything
that could escape the segment (``/``, ``?``, ``#``, control chars,
or path traversal). Returns the value unchanged on success — the
regex guarantees it is already safe to interpolate into a URL.
"""
if not isinstance(value, str) or not value:
raise ValueError("segment must be a non-empty string")
if not _AGENT_SEGMENT_RE.match(value):
raise ValueError(f"unsafe agent segment: {value!r}")
if value in (".", ".."):
raise ValueError("segment must not be '.' or '..'")
return value


def safe_url_segment(value: str) -> str:
"""Validate and percent-encode ``value`` for use as a single URL path
Expand Down
6 changes: 4 additions & 2 deletions docgrok/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -325,7 +325,8 @@ async def healthcheck_model(model_id: str):
token = cred.get_token("https://cognitiveservices.azure.com/.default").token
headers["Authorization"] = f"Bearer {token}"
except Exception:
logger.exception("failed to acquire MI token for model %s", model_id)
safe_id = str(model_id).replace("\r", " ").replace("\n", " ")[:128]
logger.exception("failed to acquire MI token for model %s", safe_id)
return {"ok": False, "status": 0, "detail": "failed to acquire managed-identity token; see server logs"}
else:
if not api_key:
Expand All @@ -337,7 +338,8 @@ async def healthcheck_model(model_id: str):
try:
r = await c.post(url, headers=headers, json={"input": "health check", "model": deployment})
except Exception:
logger.exception("healthcheck request failed for model %s", model_id)
safe_id = str(model_id).replace("\r", " ").replace("\n", " ")[:128]
logger.exception("healthcheck request failed for model %s", safe_id)
return {"ok": False, "status": 0, "detail": "request failed; see server logs"}

body = r.text[:200]
Expand Down
Loading