fix: scope GenAI/OCI config resolution to user — close cross-user fallback leaks

- _resolve_embed_config: new user_id param, scopes all fallback queries to user's own configs
- _get_adb_and_genai: propagates user_id to config resolution
- _chat_start: verify ownership of genai_config_id and oci_config_id
- TF prompt generator: scope default GenAI query to user_id
- Consult embeddings: scope GenAI resolution to user_id
- _search_cis_remediation: include global ADB configs (is_global=1)
This commit is contained in:
nogueiraguh
2026-04-01 01:30:11 -03:00
parent bc7024c95d
commit 0b49c13b4a

View File

@@ -2948,15 +2948,20 @@ def _get_adb_connection(cfg: dict):
params["tcp_connect_timeout"] = 15 # 15s connection timeout params["tcp_connect_timeout"] = 15 # 15s connection timeout
return oracledb.connect(**params) return oracledb.connect(**params)
def _resolve_embed_config(oci_config_id: str = None, genai_cfg: dict = None) -> dict: def _resolve_embed_config(oci_config_id: str = None, genai_cfg: dict = None, user_id: str = None) -> dict:
"""Resolve embedding config from genai_cfg or oci_config. Returns dict with oci_config_id, endpoint, genai_region, compartment_id.""" """Resolve embedding config from genai_cfg or oci_config. Scoped to user_id when provided.
Returns dict with oci_config_id, endpoint, genai_region, compartment_id."""
if genai_cfg: if genai_cfg:
return genai_cfg return genai_cfg
if oci_config_id: if oci_config_id:
with db() as c: with db() as c:
# Try linked genai config first # Try linked genai config first (scoped to user if provided)
gc = c.execute("SELECT * FROM genai_configs WHERE oci_config_id=? ORDER BY is_default DESC, created_at DESC", if user_id:
(oci_config_id,)).fetchone() gc = c.execute("SELECT * FROM genai_configs WHERE oci_config_id=? AND user_id=? ORDER BY is_default DESC, created_at DESC",
(oci_config_id, user_id)).fetchone()
else:
gc = c.execute("SELECT * FROM genai_configs WHERE oci_config_id=? ORDER BY is_default DESC, created_at DESC",
(oci_config_id,)).fetchone()
if gc: return dict(gc) if gc: return dict(gc)
# Fallback: build from oci_config directly # Fallback: build from oci_config directly
oc = c.execute("SELECT * FROM oci_configs WHERE id=?", (oci_config_id,)).fetchone() oc = c.execute("SELECT * FROM oci_configs WHERE id=?", (oci_config_id,)).fetchone()
@@ -2967,12 +2972,17 @@ def _resolve_embed_config(oci_config_id: str = None, genai_cfg: dict = None) ->
"endpoint": f"https://inference.generativeai.{oc['region']}.oci.oraclecloud.com", "endpoint": f"https://inference.generativeai.{oc['region']}.oci.oraclecloud.com",
"compartment_id": _safe_dec(dict(oc).get("compartment_id") or oc["tenancy_ocid"]), "compartment_id": _safe_dec(dict(oc).get("compartment_id") or oc["tenancy_ocid"]),
} }
# Last resort: any genai config # Last resort: user's own genai/oci config (never cross-user)
with db() as c: with db() as c:
gc = c.execute("SELECT * FROM genai_configs ORDER BY is_default DESC, created_at DESC").fetchone() if user_id:
gc = c.execute("SELECT * FROM genai_configs WHERE user_id=? ORDER BY is_default DESC, created_at DESC", (user_id,)).fetchone()
else:
gc = c.execute("SELECT * FROM genai_configs ORDER BY is_default DESC, created_at DESC").fetchone()
if gc: return dict(gc) if gc: return dict(gc)
# Or any oci config if user_id:
oc = c.execute("SELECT * FROM oci_configs ORDER BY created_at DESC").fetchone() oc = c.execute("SELECT * FROM oci_configs WHERE user_id=? ORDER BY created_at DESC", (user_id,)).fetchone()
else:
oc = c.execute("SELECT * FROM oci_configs ORDER BY created_at DESC").fetchone()
if oc: if oc:
return { return {
"oci_config_id": oc["id"], "oci_config_id": oc["id"],
@@ -2980,7 +2990,7 @@ def _resolve_embed_config(oci_config_id: str = None, genai_cfg: dict = None) ->
"endpoint": f"https://inference.generativeai.{oc['region']}.oci.oraclecloud.com", "endpoint": f"https://inference.generativeai.{oc['region']}.oci.oraclecloud.com",
"compartment_id": _safe_dec(dict(oc).get("compartment_id") or oc["tenancy_ocid"]), "compartment_id": _safe_dec(dict(oc).get("compartment_id") or oc["tenancy_ocid"]),
} }
raise HTTPException(400, "Nenhuma credencial OCI configurada para gerar embeddings.") raise HTTPException(400, "Nenhuma credencial OCI/GenAI configurada para gerar embeddings.")
def _embed_text(text: str, genai_cfg: dict, embedding_model_id: str, max_chars: int = 8000) -> list: def _embed_text(text: str, genai_cfg: dict, embedding_model_id: str, max_chars: int = 8000) -> list:
"""Generate embedding using OCI GenAI embed endpoint.""" """Generate embedding using OCI GenAI embed endpoint."""
@@ -4128,9 +4138,9 @@ def _chunk_text_file(text: str, filename: str, chunk_size: int = 1000, overlap:
documents.append({"content": current_chunk, "source": filename, "metadata": f"chunk: {chunk_num}"}) documents.append({"content": current_chunk, "source": filename, "metadata": f"chunk: {chunk_num}"})
return documents return documents
def _get_adb_and_genai(vid: str, oci_config_id: str = None): def _get_adb_and_genai(vid: str, oci_config_id: str = None, user_id: str = None):
"""Load ADB config and resolve embed config. """Load ADB config and resolve embed config (scoped to user_id).
Priority: ADB.genai_config_id → genai by oci_config_id → oci_config directly → any available.""" Priority: ADB.genai_config_id → genai by oci_config_id → oci_config directly → user's default."""
with db() as c: with db() as c:
cfg = c.execute("SELECT * FROM adb_vector_configs WHERE id=?", (vid,)).fetchone() cfg = c.execute("SELECT * FROM adb_vector_configs WHERE id=?", (vid,)).fetchone()
if not cfg: raise HTTPException(404, "ADB config not found") if not cfg: raise HTTPException(404, "ADB config not found")
@@ -4140,7 +4150,7 @@ def _get_adb_and_genai(vid: str, oci_config_id: str = None):
with db() as c: with db() as c:
row = c.execute("SELECT * FROM genai_configs WHERE id=?", (cfg["genai_config_id"],)).fetchone() row = c.execute("SELECT * FROM genai_configs WHERE id=?", (cfg["genai_config_id"],)).fetchone()
if row: genai_cfg = dict(row) if row: genai_cfg = dict(row)
gc = _resolve_embed_config(oci_config_id=oci_config_id, genai_cfg=genai_cfg) gc = _resolve_embed_config(oci_config_id=oci_config_id, genai_cfg=genai_cfg, user_id=user_id or cfg.get("user_id"))
return cfg, gc return cfg, gc
@app.get("/api/embeddings/preview/{rid}") @app.get("/api/embeddings/preview/{rid}")
@@ -4360,7 +4370,7 @@ async def embed_report(rid: str, req: dict, bg: BackgroundTasks, u=Depends(requi
first = next(csvmod.DictReader(f), {}) first = next(csvmod.DictReader(f), {})
extract_date = first.get("extract_date", "") extract_date = first.get("extract_date", "")
cfg, gc = _get_adb_and_genai(vid, oci_config_id=r["config_id"] if r["config_id"] else None) cfg, gc = _get_adb_and_genai(vid, oci_config_id=r["config_id"] if r["config_id"] else None, user_id=u["id"])
# Load registered tables for validation # Load registered tables for validation
with db() as c: with db() as c:
@@ -4540,7 +4550,7 @@ async def embed_report_section(rid: str, req: dict, bg: BackgroundTasks, u=Depen
first = next(csvmod.DictReader(f), {}) first = next(csvmod.DictReader(f), {})
extract_date = first.get("extract_date", "") extract_date = first.get("extract_date", "")
cfg, gc = _get_adb_and_genai(vid, oci_config_id=r["config_id"] if r["config_id"] else None) cfg, gc = _get_adb_and_genai(vid, oci_config_id=r["config_id"] if r["config_id"] else None, user_id=u["id"])
# Load registered tables for validation # Load registered tables for validation
with db() as c: with db() as c:
@@ -4667,7 +4677,7 @@ async def embed_report_file(rid: str, fid: str, req: dict, bg: BackgroundTasks,
if not content.strip(): raise HTTPException(400, "File is empty") if not content.strip(): raise HTTPException(400, "File is empty")
documents = _chunk_text_file(content, f["file_name"]) documents = _chunk_text_file(content, f["file_name"])
if not documents: raise HTTPException(400, "No content chunks found") if not documents: raise HTTPException(400, "No content chunks found")
cfg, gc = _get_adb_and_genai(vid, oci_config_id=r["config_id"] if r["config_id"] else None) cfg, gc = _get_adb_and_genai(vid, oci_config_id=r["config_id"] if r["config_id"] else None, user_id=u["id"])
target_table = req.get("table_name") or None target_table = req.get("table_name") or None
tenancy = r["tenancy_name"] or "unknown" tenancy = r["tenancy_name"] or "unknown"
task_id = str(uuid.uuid4()) task_id = str(uuid.uuid4())
@@ -4726,7 +4736,7 @@ async def embed_upload(adb_config_id: str = Form(...), table_name: str = Form(""
else: else:
documents = _chunk_text_file(content, file.filename) documents = _chunk_text_file(content, file.filename)
if not documents: raise HTTPException(400, "No content chunks found") if not documents: raise HTTPException(400, "No content chunks found")
cfg, gc = _get_adb_and_genai(adb_config_id) cfg, gc = _get_adb_and_genai(adb_config_id, user_id=u["id"])
bg.add_task(_ingest_documents_task, cfg, gc, documents, u["id"], u["username"], table_name=target_table) bg.add_task(_ingest_documents_task, cfg, gc, documents, u["id"], u["username"], table_name=target_table)
_audit(u["id"], u["username"], "embed_upload", file.filename, f"{len(documents)} chunks") _audit(u["id"], u["username"], "embed_upload", file.filename, f"{len(documents)} chunks")
return {"ok": True, "message": f"Embedding de {len(documents)} chunks iniciado", "chunks": len(documents), "filename": file.filename} return {"ok": True, "message": f"Embedding de {len(documents)} chunks iniciado", "chunks": len(documents), "filename": file.filename}
@@ -4772,7 +4782,7 @@ async def embed_upload_url(
documents = _chunk_text_file(content, url) documents = _chunk_text_file(content, url)
if not documents: if not documents:
raise HTTPException(400, "Nenhum chunk gerado do conteúdo") raise HTTPException(400, "Nenhum chunk gerado do conteúdo")
cfg, gc = _get_adb_and_genai(adb_config_id) cfg, gc = _get_adb_and_genai(adb_config_id, user_id=u["id"])
target_table = table_name.strip() or None target_table = table_name.strip() or None
bg.add_task(_ingest_documents_task, cfg, gc, documents, u["id"], u["username"], table_name=target_table) bg.add_task(_ingest_documents_task, cfg, gc, documents, u["id"], u["username"], table_name=target_table)
_audit(u["id"], u["username"], "embed_url", url, f"{len(documents)} chunks") _audit(u["id"], u["username"], "embed_url", url, f"{len(documents)} chunks")
@@ -4813,7 +4823,7 @@ async def consult_embeddings(req: ConsultQuery, u=Depends(current_user)):
rag_errors = [] rag_errors = []
for adb_cfg in adb_configs: for adb_cfg in adb_configs:
try: try:
emb_genai = _resolve_embed_config(oci_config_id=adb_cfg.get("oci_config_id")) emb_genai = _resolve_embed_config(oci_config_id=adb_cfg.get("oci_config_id"), user_id=u["id"])
except Exception as e: except Exception as e:
log.warning(f"Consult: resolve config failed for {adb_cfg['id']}: {e}") log.warning(f"Consult: resolve config failed for {adb_cfg['id']}: {e}")
continue continue
@@ -4869,15 +4879,15 @@ async def consult_embeddings(req: ConsultQuery, u=Depends(current_user)):
# Get GenAI config for answering — try saved config first, then auto-resolve from OCI # Get GenAI config for answering — try saved config first, then auto-resolve from OCI
gc = None gc = None
with db() as c: with db() as c:
gc_row = c.execute("SELECT * FROM genai_configs WHERE is_default=1 ORDER BY created_at DESC").fetchone() gc_row = c.execute("SELECT * FROM genai_configs WHERE user_id=? AND is_default=1 ORDER BY created_at DESC", (u["id"],)).fetchone()
if not gc_row: if not gc_row:
gc_row = c.execute("SELECT * FROM genai_configs ORDER BY created_at DESC").fetchone() gc_row = c.execute("SELECT * FROM genai_configs WHERE user_id=? ORDER BY created_at DESC", (u["id"],)).fetchone()
if gc_row: if gc_row:
gc = dict(gc_row) gc = dict(gc_row)
else: else:
# Auto-resolve: build GenAI config from OCI credentials + default model # Auto-resolve: build GenAI config from OCI credentials + default model
try: try:
resolved = _resolve_embed_config() resolved = _resolve_embed_config(user_id=u["id"])
gc = { gc = {
"oci_config_id": resolved["oci_config_id"], "oci_config_id": resolved["oci_config_id"],
"endpoint": resolved.get("endpoint", f"https://inference.generativeai.{resolved.get('genai_region','us-ashburn-1')}.oci.oraclecloud.com"), "endpoint": resolved.get("endpoint", f"https://inference.generativeai.{resolved.get('genai_region','us-ashburn-1')}.oci.oraclecloud.com"),
@@ -5404,7 +5414,7 @@ def _search_cis_remediation(title: str, rec_num: str, user_id: str) -> dict:
try: try:
with db() as c: with db() as c:
adb_cfgs = c.execute( adb_cfgs = c.execute(
"SELECT * FROM adb_vector_configs WHERE user_id=? AND is_active=1", (user_id,) "SELECT * FROM adb_vector_configs WHERE is_active=1 AND (user_id=? OR is_global=1)", (user_id,)
).fetchall() ).fetchall()
if not adb_cfgs: if not adb_cfgs:
return {"description": "", "remediation": ""} return {"description": "", "remediation": ""}
@@ -5425,7 +5435,7 @@ def _search_cis_remediation(title: str, rec_num: str, user_id: str) -> dict:
with db() as c: with db() as c:
row = c.execute("SELECT * FROM genai_configs WHERE id=?", (adb_cfg["genai_config_id"],)).fetchone() row = c.execute("SELECT * FROM genai_configs WHERE id=?", (adb_cfg["genai_config_id"],)).fetchone()
if row: genai_linked = dict(row) if row: genai_linked = dict(row)
emb_genai = _resolve_embed_config(genai_cfg=genai_linked) emb_genai = _resolve_embed_config(genai_cfg=genai_linked, user_id=user_id)
# Search across cisrecom + section-specific tables # Search across cisrecom + section-specific tables
all_docs = [] all_docs = []
@@ -7993,6 +8003,7 @@ def _chat_start(msg: ChatMsg, u, attachments: list = None, agent_type: str = "ch
genai_cfg = None genai_cfg = None
if msg.genai_config_id: if msg.genai_config_id:
_verify_config_access("genai", msg.genai_config_id, u)
with db() as c: with db() as c:
row = c.execute("SELECT * FROM genai_configs WHERE id=?", (msg.genai_config_id,)).fetchone() row = c.execute("SELECT * FROM genai_configs WHERE id=?", (msg.genai_config_id,)).fetchone()
if row: if row:
@@ -8005,6 +8016,7 @@ def _chat_start(msg: ChatMsg, u, attachments: list = None, agent_type: str = "ch
if msg.presence_penalty is not None: genai_cfg["presence_penalty"] = msg.presence_penalty if msg.presence_penalty is not None: genai_cfg["presence_penalty"] = msg.presence_penalty
if msg.reasoning_effort is not None: genai_cfg["reasoning_effort"] = msg.reasoning_effort if msg.reasoning_effort is not None: genai_cfg["reasoning_effort"] = msg.reasoning_effort
elif msg.model_id and msg.oci_config_id: elif msg.model_id and msg.oci_config_id:
_verify_config_access("oci", msg.oci_config_id, u)
with db() as c: with db() as c:
oci_row = c.execute("SELECT * FROM oci_configs WHERE id=?", (msg.oci_config_id,)).fetchone() oci_row = c.execute("SELECT * FROM oci_configs WHERE id=?", (msg.oci_config_id,)).fetchone()
if not oci_row: if not oci_row:
@@ -8101,7 +8113,7 @@ async def _chat_background(mid: str, sid: str, msg: ChatMsg, user: dict, genai_c
with db() as c: with db() as c:
row = c.execute("SELECT * FROM genai_configs WHERE id=?", (adb_cfg["genai_config_id"],)).fetchone() row = c.execute("SELECT * FROM genai_configs WHERE id=?", (adb_cfg["genai_config_id"],)).fetchone()
if row: genai_linked = dict(row) if row: genai_linked = dict(row)
emb_genai = _resolve_embed_config(oci_config_id=active_oci_for_rag, genai_cfg=genai_linked) emb_genai = _resolve_embed_config(oci_config_id=active_oci_for_rag, genai_cfg=genai_linked, user_id=user["id"])
if not emb_genai: if not emb_genai:
continue continue
default_model = adb_cfg.get("embedding_model_id", "cohere.embed-v4.0") default_model = adb_cfg.get("embedding_model_id", "cohere.embed-v4.0")
@@ -8922,9 +8934,9 @@ async def tf_generate_prompt(req: TfPromptReq, bg: BackgroundTasks, u=Depends(cu
} }
if not gc: if not gc:
with db() as c: with db() as c:
row = c.execute("SELECT * FROM genai_configs WHERE is_default=1").fetchone() row = c.execute("SELECT * FROM genai_configs WHERE user_id=? AND is_default=1", (u["id"],)).fetchone()
if not row: if not row:
row = c.execute("SELECT * FROM genai_configs LIMIT 1").fetchone() row = c.execute("SELECT * FROM genai_configs WHERE user_id=? LIMIT 1", (u["id"],)).fetchone()
if row: gc = dict(row) if row: gc = dict(row)
if not gc: if not gc:
raise HTTPException(400, "Nenhuma configuração GenAI disponível") raise HTTPException(400, "Nenhuma configuração GenAI disponível")