diff --git a/rag/api_core.py b/rag/api_core.py index 6e7015c..ee73e6f 100644 --- a/rag/api_core.py +++ b/rag/api_core.py @@ -1,25 +1,23 @@ # -*- coding:utf-8 -*- -"""RAG 对外 API 核心(B2B 机器接口)— dapi Bearer Key 鉴权 + org 注入。 +"""RAG 对外 API 核心(B2B 机器接口)— RBAC 会话鉴权 + dapi 平台 key 管理。 -与 UI 通道的关系: -- UI 通道(wwwroot/knowledge_bases_list/*.dspy):RBAC 登录会话鉴权,org/user 取自会话; -- API 通道(wwwroot/api/*.dspy → 本模块):路由/权限照常由 RBAC 控制 - (端点授 any),业务身份由 **dapi 模块**的 Bearer Key 决定(平台统一 key 管理, - rag 不自建 key 表)。 +认证架构(对齐 sage 范式,rag 不自建 key 体系): +- API 端点路由权限 = **logined**(见 scripts/load_path.py),鉴权统一收敛在 RBAC 中间件: + 浏览器 cookie 会话 或 `Authorization: Bearer ` 都能过门 + (dapi.load_dapi() 注册 'Bearer '→bearer_auth,认证通过后 dapi 建立会话); +- 未认证 → RBAC 直接 401/403,**不进 dspy**; +- dspy 用 session_env(request) 从请求会话取 org/user,机构隔离在 rag 边界强制。 -鉴权链路:调用方在 dapi 申请 downapikey(绑定 users 账号)→ 本模块 verify_api_key() -调 dapi.get_apikey_user 校验(有效性/过期/IP白名单都在 dapi)→ 从认证用户取 orgid -→ 机构隔离以该 org 为边界。 +key 生命周期(生成、过期、IP 白名单)全归平台 dapi 模块 +(downapp/downapikey 表 + key 申请管理 UI),rag 只消费认证结果。 +downappuser 角色不授 rag 权限,仅作为该调用方的 RBAC 身份。 -org 注入方式:构造一个轻量 env(DictObject),get_userorgid/get_user 返回 key 用户所属 -机构,get_module_dbname 等透传全局 ServerEnv —— 复用 init.py 的底层能力 -(_resolve_search_kbs/_build_search_vector/_call_uapi 等),杜绝双实现分叉。 +org 注入方式(session_env):返回真实请求 _run_ns,get_userorgid/get_user 走会话读取 +(auth.remember 的签名 cookie,bearer_auth 在权限校验阶段已建立)—— 与 UI 通道同一 +机制,复用 init.py 底层能力(_resolve_search_kbs/_build_search_vector/_call_uapi 等)。 -权限语义(v1): -- key 即用户级凭据:org 隔离强制(只能操作本机构知识库),能力面等于该用户在本机构 - 的知识库操作范围;不再做 rag 侧 scopes(授权粒度归 dapi 账号管理)。 - -2026-09-03 新增(配合 wwwroot/api/ 七个对外端点;key 管理复用 dapi 模块)。 +历史:2026-09-03 初版在 dspy 内用 rag 自建 rag_api_keys 表校验 Bearer Key; +按「apikey 统一由 dapi 管理 + 路由至少 logined」重构为本方案,verify_api_key 已删除。 """ import json import os @@ -30,39 +28,15 @@ from appPublic.dictObject import DictObject from sqlor.dbpools import get_sor_context -# ────────────────────────── API Key 鉴权(dapi 统一管理) ────────────────────────── +# ────────────────────────── 会话 env(org/user 取自认证会话) ────────────────────────── -async def verify_api_key(request): - """用 dapi 模块验证调用方 Bearer Key(downapp/downapikey/users 三表)。 +def session_env(request): + """API 端点的业务 env:直接返回真实请求的运行 env(org 从会话取)。 - 平台级机制:key 有效性、过期、IP 白名单、登录态、downappuser 角色 全由 - dapi.apikey_user 负责;rag 只消费鉴权结果,从认证用户反查 org_id。 - - 返回 (ctx_dict, None) 或 (None, error_message)。 - ctx = {org_id, user_id, username} + 未认证请求到不了这里(RBAC logined 在路由层把关)。保留本函数作为统一入口, + 将来需要加 API 侧横切(限流头/审计标签)时只改这里。 """ - auth = request.headers.get('Authorization', '') or '' - token = auth[7:].strip() if auth.startswith('Bearer ') else '' - if not token: - token = (request.headers.get('x-api-key', '') or '').strip() - if not token: - return None, "missing api key(Authorization: Bearer *** 或 x-api-key 头)" - - from dapi.dapi import get_apikey_user - - env = ServerEnv() - dbname = env.get_module_dbname('dapi') - client_ip = (request.get('client_ip') if hasattr(request, 'get') else '') or '' - async with get_sor_context(env, dbname) as sor: - user = await get_apikey_user(sor, token, client_ip) - await sor.sqlExe("COMMIT", {}) - if user is None: - return None, "invalid api key" - org_id = str(getattr(user, 'orgid', '') or getattr(user, 'org_id', '') or '') - if not org_id: - return None, "api key user has no org" - return {"org_id": org_id, "user_id": str(getattr(user, 'id', '') or ''), - "username": str(getattr(user, 'username', '') or '')}, None + return request._run_ns async def read_json_body(request, params_kw): @@ -83,10 +57,14 @@ async def read_json_body(request, params_kw): return dict(params_kw or {}) -# ────────────────────────── org 注入 env ────────────────────────── +# ────────────────────────── org 注入 env(内部 tools 通道专用) ────────────────────────── def make_api_env(ctx): - """轻量 env:org 取自 API key,其余透传全局 ServerEnv。""" + """轻量 env:org 由调用方(内部助手宿主)注入,其余透传全局 ServerEnv。 + + HTTP API 通道用 session_env(request)(会话身份);内部 tools 通道没有 HTTP + 请求,宿主助手注入可信 org_id 走本函数。 + """ g = ServerEnv() async def _orgid(): diff --git a/scripts/load_path.py b/scripts/load_path.py index 9deb011..f86119f 100644 --- a/scripts/load_path.py +++ b/scripts/load_path.py @@ -13,17 +13,9 @@ PATHS_ANY = [ f"/{MOD}/knowledge_bases_list/upload.js", ] -# 对外 B2B API 端点(路由权限授 any;业务鉴权靠 dapi 模块统一管理的 Bearer Key, -# org 隔离在 rag/api_core.py 内强制——见模块 README「对外 API」) -PATHS_API_ANY = [ - f"/{MOD}/api/kb_create.dspy", - f"/{MOD}/api/kb_delete.dspy", - f"/{MOD}/api/doc_upload.dspy", - f"/{MOD}/api/doc_delete.dspy", - f"/{MOD}/api/tag_create.dspy", - f"/{MOD}/api/doc_set_tags.dspy", - f"/{MOD}/api/search.dspy", -] +# 对外 B2B API 端点 → 已并入 PATHS_LOGINED(路由至少 logined): +# cookie 会话 或 Authorization: Bearer (dapi 平台统一 key 管理) +# 均可过 RBAC 门;org 隔离在 rag/api_core.py 的会话身份内强制。 # 登录用户可访问(知识库管理全部端点) PATHS_LOGINED = [ @@ -64,6 +56,14 @@ PATHS_LOGINED = [ f"/{MOD}/knowledge_bases_list/storage_card.dspy", f"/{MOD}/knowledge_bases_list/storage_stats.dspy", # (API Key 管理走平台 dapi 模块:/dapi/ 的 key 申请/管理 UI,rag 不自建 key 端点) + # 对外 B2B API(认证=会话或 Bearer key,授权=logined) + f"/{MOD}/api/kb_create.dspy", + f"/{MOD}/api/kb_delete.dspy", + f"/{MOD}/api/doc_upload.dspy", + f"/{MOD}/api/doc_delete.dspy", + f"/{MOD}/api/tag_create.dspy", + f"/{MOD}/api/doc_set_tags.dspy", + f"/{MOD}/api/search.dspy", # CRUD 管理页 f"/{MOD}/documents_list/index.ui", f"/{MOD}/engine_configs_list/index.ui", diff --git a/wwwroot/api/doc_delete.dspy b/wwwroot/api/doc_delete.dspy index 7a766dd..d98cfb5 100644 --- a/wwwroot/api/doc_delete.dspy +++ b/wwwroot/api/doc_delete.dspy @@ -1,10 +1,7 @@ # 对外 API:文件删除(级联清理向量/chunks/文件) -# POST /rag/api/doc_delete.dspy Authorization: Bearer +# POST /rag/api/doc_delete.dspy 认证:cookie 会话或 Authorization: Bearer ,RBAC logined 把关 # body/query: {doc_id} from rag import api_core as C -_ctx, _e = await C.verify_api_key(request) -if _e: - return C._err(_e, code="unauthorized") _ns = await C.read_json_body(request, params_kw) -env = C.make_api_env(_ctx) +env = C.session_env(request) return await C.doc_delete(env, _ns) diff --git a/wwwroot/api/doc_set_tags.dspy b/wwwroot/api/doc_set_tags.dspy index 3ce88f5..ce4f3bb 100644 --- a/wwwroot/api/doc_set_tags.dspy +++ b/wwwroot/api/doc_set_tags.dspy @@ -1,10 +1,7 @@ # 对外 API:文件设置标签(全量语义,标签名不存在自动创建) -# POST /rag/api/doc_set_tags.dspy Authorization: Bearer +# POST /rag/api/doc_set_tags.dspy 认证:cookie 会话或 Authorization: Bearer ,RBAC logined 把关 # body/query: {kb_id, doc_id, tags:[名称...] 或 "a,b" | tag_ids:[id...] 或 "id1,id2"} from rag import api_core as C -_ctx, _e = await C.verify_api_key(request) -if _e: - return C._err(_e, code="unauthorized") _ns = await C.read_json_body(request, params_kw) -env = C.make_api_env(_ctx) +env = C.session_env(request) return await C.doc_set_tags(env, _ns) diff --git a/wwwroot/api/doc_upload.dspy b/wwwroot/api/doc_upload.dspy index 135309b..b2034c6 100644 --- a/wwwroot/api/doc_upload.dspy +++ b/wwwroot/api/doc_upload.dspy @@ -1,12 +1,9 @@ # 对外 API:文件上传(原始字节 body) -# POST /rag/api/doc_upload.dspy?kb_id=X&file_name=Y Authorization: Bearer +# POST /rag/api/doc_upload.dspy?kb_id=X&file_name=Y 认证:cookie 会话或 Authorization: Bearer ,RBAC logined 把关 # Content-Type: application/octet-stream,请求体即文件原始字节 from rag import api_core as C -_ctx, _e = await C.verify_api_key(request) -if _e: - return C._err(_e, code="unauthorized") _ns = dict(params_kw or {}) _file_data = await request.read() _file_name = _ns.get('file_name', '') or 'upload.bin' -env = C.make_api_env(_ctx) +env = C.session_env(request) return await C.doc_upload(env, _ns, _file_data, _file_name) diff --git a/wwwroot/api/kb_create.dspy b/wwwroot/api/kb_create.dspy index 4d7642d..281dd35 100644 --- a/wwwroot/api/kb_create.dspy +++ b/wwwroot/api/kb_create.dspy @@ -1,10 +1,7 @@ # 对外 API:创建知识库 -# POST /rag/api/kb_create.dspy Authorization: Bearer +# POST /rag/api/kb_create.dspy 认证:cookie 会话或 Authorization: Bearer ,RBAC logined 把关 # body(JSON 或 query): {name, description?, embedding_engine?} from rag import api_core as C -_ctx, _e = await C.verify_api_key(request) -if _e: - return C._err(_e, code="unauthorized") _ns = await C.read_json_body(request, params_kw) -env = C.make_api_env(_ctx) +env = C.session_env(request) return await C.kb_create(env, _ns) diff --git a/wwwroot/api/kb_delete.dspy b/wwwroot/api/kb_delete.dspy index 5827b7d..80303ae 100644 --- a/wwwroot/api/kb_delete.dspy +++ b/wwwroot/api/kb_delete.dspy @@ -1,10 +1,7 @@ # 对外 API:删除知识库(全量级联清理) -# POST /rag/api/kb_delete.dspy Authorization: Bearer +# POST /rag/api/kb_delete.dspy 认证:cookie 会话或 Authorization: Bearer ,RBAC logined 把关 # body/query: {kb_id} from rag import api_core as C -_ctx, _e = await C.verify_api_key(request) -if _e: - return C._err(_e, code="unauthorized") _ns = await C.read_json_body(request, params_kw) -env = C.make_api_env(_ctx) +env = C.session_env(request) return await C.kb_delete(env, _ns) diff --git a/wwwroot/api/search.dspy b/wwwroot/api/search.dspy index 5e9ddbb..d66c27f 100644 --- a/wwwroot/api/search.dspy +++ b/wwwroot/api/search.dspy @@ -1,10 +1,7 @@ # 对外 API:知识库检索(向量召回 + 重排;kb_id 缺省=本机构全部知识库) -# POST /rag/api/search.dspy Authorization: Bearer +# POST /rag/api/search.dspy 认证:cookie 会话或 Authorization: Bearer ,RBAC logined 把关 # body/query: {query, kb_id?, top_k?(默认10), recall_k?(默认top_k*3)} from rag import api_core as C -_ctx, _e = await C.verify_api_key(request) -if _e: - return C._err(_e, code="unauthorized") _ns = await C.read_json_body(request, params_kw) -env = C.make_api_env(_ctx) +env = C.session_env(request) return await C.search(env, _ns) diff --git a/wwwroot/api/tag_create.dspy b/wwwroot/api/tag_create.dspy index 9642149..e67d959 100644 --- a/wwwroot/api/tag_create.dspy +++ b/wwwroot/api/tag_create.dspy @@ -1,10 +1,7 @@ # 对外 API:创建标签(同名幂等) -# POST /rag/api/tag_create.dspy Authorization: Bearer +# POST /rag/api/tag_create.dspy 认证:cookie 会话或 Authorization: Bearer ,RBAC logined 把关 # body/query: {kb_id, name, color?} from rag import api_core as C -_ctx, _e = await C.verify_api_key(request) -if _e: - return C._err(_e, code="unauthorized") _ns = await C.read_json_body(request, params_kw) -env = C.make_api_env(_ctx) +env = C.session_env(request) return await C.tag_create(env, _ns)