From 0b49c13b4a4a459a1c9c14798a8a3d9e4bfb34d2 Mon Sep 17 00:00:00 2001 From: nogueiraguh Date: Wed, 1 Apr 2026 01:30:11 -0300 Subject: [PATCH] =?UTF-8?q?fix:=20scope=20GenAI/OCI=20config=20resolution?= =?UTF-8?q?=20to=20user=20=E2=80=94=20close=20cross-user=20fallback=20leak?= =?UTF-8?q?s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - _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) --- backend/app.py | 68 +++++++++++++++++++++++++++++--------------------- 1 file changed, 40 insertions(+), 28 deletions(-) diff --git a/backend/app.py b/backend/app.py index 8e6994f..c102a25 100644 --- a/backend/app.py +++ b/backend/app.py @@ -2948,15 +2948,20 @@ def _get_adb_connection(cfg: dict): params["tcp_connect_timeout"] = 15 # 15s connection timeout return oracledb.connect(**params) -def _resolve_embed_config(oci_config_id: str = None, genai_cfg: dict = None) -> dict: - """Resolve embedding config from genai_cfg or oci_config. Returns dict with oci_config_id, endpoint, genai_region, compartment_id.""" +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. Scoped to user_id when provided. + Returns dict with oci_config_id, endpoint, genai_region, compartment_id.""" if genai_cfg: return genai_cfg if oci_config_id: with db() as c: - # Try linked genai config first - gc = c.execute("SELECT * FROM genai_configs WHERE oci_config_id=? ORDER BY is_default DESC, created_at DESC", - (oci_config_id,)).fetchone() + # Try linked genai config first (scoped to user if provided) + if user_id: + 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) # Fallback: build from oci_config directly 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", "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: - 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) - # Or any oci config - oc = c.execute("SELECT * FROM oci_configs ORDER BY created_at DESC").fetchone() + if user_id: + 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: return { "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", "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: """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}"}) return documents -def _get_adb_and_genai(vid: str, oci_config_id: str = None): - """Load ADB config and resolve embed config. - Priority: ADB.genai_config_id → genai by oci_config_id → oci_config directly → any available.""" +def _get_adb_and_genai(vid: str, oci_config_id: str = None, user_id: str = None): + """Load ADB config and resolve embed config (scoped to user_id). + Priority: ADB.genai_config_id → genai by oci_config_id → oci_config directly → user's default.""" with db() as c: cfg = c.execute("SELECT * FROM adb_vector_configs WHERE id=?", (vid,)).fetchone() 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: row = c.execute("SELECT * FROM genai_configs WHERE id=?", (cfg["genai_config_id"],)).fetchone() 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 @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), {}) 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 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), {}) 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 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") documents = _chunk_text_file(content, f["file_name"]) 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 tenancy = r["tenancy_name"] or "unknown" task_id = str(uuid.uuid4()) @@ -4726,7 +4736,7 @@ async def embed_upload(adb_config_id: str = Form(...), table_name: str = Form("" else: documents = _chunk_text_file(content, file.filename) 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) _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} @@ -4772,7 +4782,7 @@ async def embed_upload_url( documents = _chunk_text_file(content, url) if not documents: 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 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") @@ -4813,7 +4823,7 @@ async def consult_embeddings(req: ConsultQuery, u=Depends(current_user)): rag_errors = [] for adb_cfg in adb_configs: 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: log.warning(f"Consult: resolve config failed for {adb_cfg['id']}: {e}") 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 gc = None 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: - 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: gc = dict(gc_row) else: # Auto-resolve: build GenAI config from OCI credentials + default model try: - resolved = _resolve_embed_config() + resolved = _resolve_embed_config(user_id=u["id"]) gc = { "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"), @@ -5404,7 +5414,7 @@ def _search_cis_remediation(title: str, rec_num: str, user_id: str) -> dict: try: with db() as c: 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() if not adb_cfgs: return {"description": "", "remediation": ""} @@ -5425,7 +5435,7 @@ def _search_cis_remediation(title: str, rec_num: str, user_id: str) -> dict: with db() as c: row = c.execute("SELECT * FROM genai_configs WHERE id=?", (adb_cfg["genai_config_id"],)).fetchone() 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 all_docs = [] @@ -7993,6 +8003,7 @@ def _chat_start(msg: ChatMsg, u, attachments: list = None, agent_type: str = "ch genai_cfg = None if msg.genai_config_id: + _verify_config_access("genai", msg.genai_config_id, u) with db() as c: row = c.execute("SELECT * FROM genai_configs WHERE id=?", (msg.genai_config_id,)).fetchone() 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.reasoning_effort is not None: genai_cfg["reasoning_effort"] = msg.reasoning_effort elif msg.model_id and msg.oci_config_id: + _verify_config_access("oci", msg.oci_config_id, u) with db() as c: oci_row = c.execute("SELECT * FROM oci_configs WHERE id=?", (msg.oci_config_id,)).fetchone() 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: row = c.execute("SELECT * FROM genai_configs WHERE id=?", (adb_cfg["genai_config_id"],)).fetchone() 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: continue 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: 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: - 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 not gc: raise HTTPException(400, "Nenhuma configuração GenAI disponível")