mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-07-14 00:48:26 +08:00
### Related issues Closes #14781 ### What problem does this PR solve? Some retrieval endpoints accepted caller-supplied `tenant_rerank_id` and resolved it through `get_model_config_by_id(...)`. That helper loaded `TenantLLM` rows by global database id and returned decoded model configuration without checking whether the model belonged to the authenticated tenant or the dataset owner tenant. This meant dataset access was validated, but rerank-model selection was not. A caller who knew or could guess another tenant's `tenant_rerank_id` could attempt retrieval with a foreign rerank model config, creating a cross-tenant authorization gap for model usage. This PR closes that gap by making `tenant_rerank_id` resolution tenant-aware across the retrieval paths that accept it. ### Type of change - [x] Bug Fix (non-breaking change which fixes an issue) - [ ] New Feature (non-breaking change which adds functionality) - [ ] Documentation Update - [ ] Refactoring - [ ] Performance Improvement - [ ] Other (please describe): ### Solution - Extend `get_model_config_by_id(...)` to accept an optional `allowed_tenant_ids` set and reject `TenantLLM` rows whose `tenant_id` is outside that set. - Pass the allowed tenant scope from retrieval endpoints that accept `tenant_rerank_id`: - `api/apps/sdk/doc.py` - `api/apps/sdk/session.py` - `api/apps/services/dataset_api_service.py` - Use the authenticated tenant plus dataset-owner tenant ids already derived by each retrieval flow as the authorization boundary for rerank model selection. - Add focused unit coverage to assert unauthorized `tenant_rerank_id` values are rejected and that the allowed tenant set is propagated correctly. ### Testing - `python -m py_compile` on: - `api/db/joint_services/tenant_model_service.py` - `api/apps/services/dataset_api_service.py` - `api/apps/sdk/doc.py` - `api/apps/sdk/session.py` - Added unit tests in: - `test/testcases/test_http_api/test_file_management_within_dataset/test_doc_sdk_routes_unit.py` - `test/testcases/test_http_api/test_session_management/test_session_sdk_routes_unit.py` ### Notes for reviewers - This change is intentionally narrow: it affects only the `tenant_rerank_id` path, not the normal `rerank_id` name-based resolution path. - Local lint/syntax checks passed. - Full pytest execution could not be completed in this environment because the local test runtime is missing `strenum`, so the route-test files fail during collection before exercising the updated cases. --------- Co-authored-by: jony376 <jony376@gmail.com>
171 lines
8.2 KiB
Python
171 lines
8.2 KiB
Python
#
|
|
# Copyright 2026 The InfiniFlow Authors. All Rights Reserved.
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
#
|
|
import logging
|
|
import os
|
|
import enum
|
|
from common import settings
|
|
from common.constants import LLMType
|
|
from api.db.services.llm_service import LLMService
|
|
from api.db.services.tenant_llm_service import TenantLLMService, TenantService
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
def get_model_config_by_id(
|
|
tenant_model_id: int,
|
|
allowed_tenant_ids: str | list[str] | set[str] | tuple[str, ...] | None = None,
|
|
requester_tenant_id: str | None = None,
|
|
) -> dict:
|
|
found, model_config = TenantLLMService.get_by_id(tenant_model_id)
|
|
if not found:
|
|
raise LookupError(f"Tenant Model with id {tenant_model_id} not found")
|
|
if allowed_tenant_ids is not None:
|
|
if isinstance(allowed_tenant_ids, str):
|
|
allowed_tenant_ids = {allowed_tenant_ids}
|
|
else:
|
|
allowed_tenant_ids = {str(tenant_id) for tenant_id in allowed_tenant_ids if tenant_id}
|
|
if str(model_config.tenant_id) not in allowed_tenant_ids:
|
|
logger.warning(
|
|
"Denied tenant model access: tenant_model_id=%s model_tenant_id=%s "
|
|
"allowed_tenant_ids=%s requester_tenant_id=%s",
|
|
tenant_model_id,
|
|
model_config.tenant_id,
|
|
sorted(allowed_tenant_ids),
|
|
requester_tenant_id,
|
|
)
|
|
raise LookupError(f"Tenant Model with id {tenant_model_id} not authorized")
|
|
config_dict = model_config.to_dict()
|
|
api_key, is_tools, api_key_payload = TenantLLMService._decode_api_key_config(config_dict.get("api_key", ""))
|
|
config_dict["api_key"] = api_key
|
|
if api_key_payload is not None:
|
|
config_dict["api_key_payload"] = api_key_payload
|
|
if is_tools is not None:
|
|
config_dict["is_tools"] = is_tools
|
|
llm = LLMService.query(llm_name=config_dict["llm_name"])
|
|
if "is_tools" not in config_dict and llm:
|
|
config_dict["is_tools"] = llm[0].is_tools
|
|
return config_dict
|
|
|
|
|
|
def get_model_config_by_type_and_name(tenant_id: str, model_type: str, model_name: str):
|
|
if not model_name:
|
|
raise Exception("Model Name is required")
|
|
model_type_val = model_type.value if hasattr(model_type, "value") else model_type
|
|
model_config = TenantLLMService.get_api_key(tenant_id, model_name, model_type_val)
|
|
if not model_config:
|
|
# model_name in format 'name@factory', split model_name and try again
|
|
pure_model_name, fid = TenantLLMService.split_model_name_and_factory(model_name)
|
|
compose_profiles = os.getenv("COMPOSE_PROFILES", "")
|
|
is_tei_builtin_embedding = (
|
|
model_type_val == LLMType.EMBEDDING.value
|
|
and "tei-" in compose_profiles
|
|
and pure_model_name == os.getenv("TEI_MODEL", "")
|
|
and (fid == "Builtin" or fid is None)
|
|
)
|
|
if is_tei_builtin_embedding:
|
|
# configured local embedding model
|
|
embedding_cfg = settings.EMBEDDING_CFG
|
|
config_dict = {
|
|
"llm_factory": "Builtin",
|
|
"api_key": embedding_cfg["api_key"],
|
|
"llm_name": pure_model_name,
|
|
"api_base": embedding_cfg["base_url"],
|
|
"model_type": LLMType.EMBEDDING.value,
|
|
}
|
|
elif model_type_val == LLMType.CHAT.value:
|
|
# Retry as CHAT with pure_model_name first; then fall back to a multimodal model registered under IMAGE2TEXT.
|
|
model_config = TenantLLMService.get_api_key(tenant_id, pure_model_name, LLMType.CHAT.value)
|
|
if not model_config:
|
|
model_config = TenantLLMService.get_api_key(tenant_id, pure_model_name, LLMType.IMAGE2TEXT.value)
|
|
if not model_config:
|
|
raise LookupError(f"Tenant Model with name {model_name} and type {model_type_val} not found")
|
|
config_dict = model_config.to_dict()
|
|
elif model_type_val == LLMType.IMAGE2TEXT.value:
|
|
model_config = TenantLLMService.get_api_key(tenant_id, pure_model_name, LLMType.IMAGE2TEXT.value)
|
|
if not model_config:
|
|
# Fall back to a chat model only if it has declared IMAGE2TEXT capability (tag check via llm table)
|
|
chat_config = TenantLLMService.get_api_key(tenant_id, pure_model_name, LLMType.CHAT.value)
|
|
logger.debug("IMAGE2TEXT config not found for %s; chat_config found: %s", pure_model_name, chat_config is not None)
|
|
if chat_config:
|
|
llm_entry = LLMService.query(fid=chat_config.llm_factory, llm_name=chat_config.llm_name)
|
|
tags = [t.strip() for t in (llm_entry[0].tags or "").split(",")] if llm_entry else []
|
|
logger.debug("LLM tags for %s/%s: %s", chat_config.llm_factory, chat_config.llm_name, tags)
|
|
if "IMAGE2TEXT" in tags:
|
|
logger.debug("Promoting chat config to IMAGE2TEXT for %s", pure_model_name)
|
|
model_config = chat_config
|
|
if not model_config:
|
|
raise LookupError(f"Tenant Model with name {model_name} and type {model_type_val} not found")
|
|
config_dict = model_config.to_dict()
|
|
config_dict["model_type"] = LLMType.IMAGE2TEXT.value
|
|
else:
|
|
model_config = TenantLLMService.get_api_key(tenant_id, pure_model_name, model_type_val)
|
|
if not model_config:
|
|
raise LookupError(f"Tenant Model with name {model_name} and type {model_type_val} not found")
|
|
config_dict = model_config.to_dict()
|
|
else:
|
|
# model_name without @factory
|
|
config_dict = model_config.to_dict()
|
|
api_key, is_tools, api_key_payload = TenantLLMService._decode_api_key_config(config_dict.get("api_key", ""))
|
|
config_dict["api_key"] = api_key
|
|
if api_key_payload is not None:
|
|
config_dict["api_key_payload"] = api_key_payload
|
|
if is_tools is not None:
|
|
config_dict["is_tools"] = is_tools
|
|
config_model_type = config_dict.get("model_type")
|
|
config_model_type = config_model_type.value if hasattr(config_model_type, "value") else config_model_type
|
|
if config_model_type != model_type_val and not (
|
|
model_type_val == LLMType.CHAT.value
|
|
and config_model_type == LLMType.IMAGE2TEXT.value
|
|
) and not (
|
|
model_type_val == LLMType.IMAGE2TEXT.value
|
|
and config_model_type == LLMType.CHAT.value
|
|
):
|
|
raise LookupError(
|
|
f"Tenant Model with name {model_name} has type {config_model_type}, expected {model_type_val}"
|
|
)
|
|
llm = LLMService.query(llm_name=config_dict["llm_name"])
|
|
if "is_tools" not in config_dict and llm:
|
|
config_dict["is_tools"] = llm[0].is_tools
|
|
return config_dict
|
|
|
|
|
|
def get_tenant_default_model_by_type(tenant_id: str, model_type: str|enum.Enum):
|
|
exist, tenant = TenantService.get_by_id(tenant_id)
|
|
if not exist:
|
|
raise LookupError("Tenant not found")
|
|
model_type_val = model_type if isinstance(model_type, str) else model_type.value
|
|
model_name: str = ""
|
|
match model_type_val:
|
|
case LLMType.EMBEDDING.value:
|
|
model_name = tenant.embd_id
|
|
case LLMType.SPEECH2TEXT.value:
|
|
model_name = tenant.asr_id
|
|
case LLMType.IMAGE2TEXT.value:
|
|
model_name = tenant.img2txt_id
|
|
case LLMType.CHAT.value:
|
|
model_name = tenant.llm_id
|
|
case LLMType.RERANK.value:
|
|
model_name = tenant.rerank_id
|
|
case LLMType.TTS.value:
|
|
model_name = tenant.tts_id
|
|
case LLMType.OCR.value:
|
|
raise Exception("OCR model name is required")
|
|
case _:
|
|
raise Exception(f"Unknown model type {model_type}")
|
|
if not model_name:
|
|
raise Exception(f"No default {model_type} model is set.")
|
|
return get_model_config_by_type_and_name(tenant_id, model_type, model_name)
|