refactor(rag-api): 鉴权收敛 RBAC 会话,API 端点改授 logined

- 端点权限 any -> logined(用户纠偏:路由至少 logined)
- 认证统一由 RBAC 中间件把关:cookie 会话 或 Bearer <dapi key>
 (dapi.bearer_auth 注册在 registered_auth_methods,认证后建立会话),
  未认证到不了 dspy,删除 verify_api_key 自查
- dspy 用 C.session_env(request) 取真实请求 env(org 走会话),
  与 UI 通道同一身份机制
- make_api_env 保留给内部 tools 通道(宿主注入可信 org,无 HTTP 会话)
This commit is contained in:
ymq 2026-09-03 16:11:27 +08:00
parent 51b0bf1b85
commit 282845bbf1
9 changed files with 52 additions and 95 deletions

View File

@ -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/*.dspyRBAC 登录会话鉴权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 <downapikey>` 都能过门
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 申请管理 UIrag 只消费认证结果
downappuser 角色不授 rag 权限仅作为该调用方的 RBAC 身份
org 注入方式构造一个轻量 envDictObjectget_userorgid/get_user 返回 key 用户所属
机构get_module_dbname 等透传全局 ServerEnv 复用 init.py 的底层能力
_resolve_search_kbs/_build_search_vector/_call_uapi 杜绝双实现分叉
org 注入方式session_env返回真实请求 _run_nsget_userorgid/get_user 走会话读取
auth.remember 的签名 cookiebearer_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 统一管理 ──────────────────────────
# ────────────────────────── 会话 envorg/user 取自认证会话 ──────────────────────────
async def verify_api_key(request):
"""用 dapi 模块验证调用方 Bearer Keydownapp/downapikey/users 三表)。
def session_env(request):
"""API 端点的业务 env直接返回真实请求的运行 envorg 从会话取)。
平台级机制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 keyAuthorization: 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):
"""轻量 envorg 取自 API key其余透传全局 ServerEnv。"""
"""轻量 envorg 由调用方(内部助手宿主)注入,其余透传全局 ServerEnv。
HTTP API 通道用 session_env(request)会话身份内部 tools 通道没有 HTTP
请求宿主助手注入可信 org_id 走本函数
"""
g = ServerEnv()
async def _orgid():

View File

@ -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>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 申请/管理 UIrag 不自建 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",

View File

@ -1,10 +1,7 @@
# 对外 API文件删除级联清理向量/chunks/文件)
# POST /rag/api/doc_delete.dspy Authorization: Bearer <api-key>
# POST /rag/api/doc_delete.dspy 认证cookie 会话或 Authorization: Bearer <dapi key>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)

View File

@ -1,10 +1,7 @@
# 对外 API文件设置标签全量语义标签名不存在自动创建
# POST /rag/api/doc_set_tags.dspy Authorization: Bearer <api-key>
# POST /rag/api/doc_set_tags.dspy 认证cookie 会话或 Authorization: Bearer <dapi key>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)

View File

@ -1,12 +1,9 @@
# 对外 API文件上传原始字节 body
# POST /rag/api/doc_upload.dspy?kb_id=X&file_name=Y Authorization: Bearer <api-key>
# POST /rag/api/doc_upload.dspy?kb_id=X&file_name=Y 认证cookie 会话或 Authorization: Bearer <dapi key>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)

View File

@ -1,10 +1,7 @@
# 对外 API创建知识库
# POST /rag/api/kb_create.dspy Authorization: Bearer <api-key>
# POST /rag/api/kb_create.dspy 认证cookie 会话或 Authorization: Bearer <dapi key>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)

View File

@ -1,10 +1,7 @@
# 对外 API删除知识库全量级联清理
# POST /rag/api/kb_delete.dspy Authorization: Bearer <api-key>
# POST /rag/api/kb_delete.dspy 认证cookie 会话或 Authorization: Bearer <dapi key>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)

View File

@ -1,10 +1,7 @@
# 对外 API知识库检索向量召回 + 重排kb_id 缺省=本机构全部知识库)
# POST /rag/api/search.dspy Authorization: Bearer <api-key>
# POST /rag/api/search.dspy 认证cookie 会话或 Authorization: Bearer <dapi key>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)

View File

@ -1,10 +1,7 @@
# 对外 API创建标签同名幂等
# POST /rag/api/tag_create.dspy Authorization: Bearer <api-key>
# POST /rag/api/tag_create.dspy 认证cookie 会话或 Authorization: Bearer <dapi key>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)