在不触碰 RAGFlow 任何一张表、任何一行业务逻辑的前提下,外挂一个独立的权限代理服务,把企业级的细粒度权限模型「翻译」成 RAGFlow 能理解的原生原语。
先把 RAGFlow 现状摸清——这是「不改原逻辑」的前提。以下均为 0.25.6 实际源码。
class UserTenantRole(StrEnum):
OWNER = 'owner' # 租户所有者(创建者)
ADMIN = 'admin' # 租户管理员
NORMAL = 'normal' # 普通成员
INVITE = 'invite' # 已邀请未接受
class TenantPermission(StrEnum):
ME = 'me' # 仅自己可见
TEAM = 'team' # 团队可见
| 表 | 关键字段 | 含义 | 源码位置 |
|---|---|---|---|
user | access_token, is_superuser | 登录令牌;是否 root | db_models.py:711,725 |
tenant | id, credit | 租户=工作区;credit 是唯一的额度字段 | db_models.py:750,767 |
user_tenant | user_id,tenant_id,role | 用户-租户多对多 + 角色 | db_models.py:774-778 |
knowledgebase | tenant_id,created_by,permission | 归属租户/创建者/可见性(me|team) | db_models.py:869-876 |
api_token | tenant_id,token,dialog_id | API Key,按租户发放 | db_models.py:1024-1033 |
permission = CharField(max_length=16, null=False, help_text="me|team",
default="me", index=True) # ← 知识库可见性,仅二态
这是 RAGFlow 决定「谁能看到哪个 KB」的全部逻辑——只有两条规则:① 是我建的;② 团队共享且我在该租户里。
api/db/services/knowledgebase_service.py:52 / 485# 列表过滤(构造 Peewee 查询条件)
def _visibility_and_status_filter(cls, joined_tenant_ids, user_id):
return (
( (cls.model.tenant_id.in_(joined_tenant_ids)
& (cls.model.permission == TenantPermission.TEAM.value)) # 团队共享
| (cls.model.tenant_id == user_id) ) # 自己的工作区
& (cls.model.status == StatusEnum.VALID.value)
)
# 单个 KB 访问判定
def accessible(cls, kb_id, user_id):
e, kb = cls.get_by_id(kb_id)
if kb.tenant_id == user_id: return True # 创建者
if kb.permission != TenantPermission.TEAM.value: return False
joined = TenantService.get_joined_tenants_by_user_id(user_id)
return any(t["tenant_id"] == kb.tenant_id for t in joined) # 同团队 + team 可见
permission=team 是全租户广播,粒度太粗——这正是代理层要补的核心缺口。
RAGFlow 所有请求经 _load_user 解析身份,它同时支持 JWT(access_token) 和 APIToken 两条路径——这是代理服务最理想的「挂载缝隙」。
def _load_user():
authorization = request.headers.get("Authorization")
# 路径①:JWT 解出 access_token → 查 user 表
access_token = str(jwt.loads(auth_token))
user = UserService.query(access_token=access_token, status=VALID)
...
# 路径②:当作 APIToken → 用 tenant_id 反查 user(系统应用走这里)
objs = APIToken.query(token=auth_token)
user = UserService.query(id=objs[0].tenant_id, status=VALID)
g.auth_via_api_token = True
MAX_FILE_NUM_PER_USER = int(os.environ.get("MAX_FILE_NUM_PER_USER", 0))
if 0 < MAX_FILE_NUM_PER_USER <= DocumentService.get_doc_count(tenant_id):
raise RuntimeError("Exceed the maximum file number of a free user!")
全局一刀切,无法按角色/机构/KB 分级——配额管控需要在代理层重做。
| 需求 | RAGFlow 原生 | 差距 | 代理层补法 |
|---|---|---|---|
| 五级角色 | 租户内 4 角色,无业务分级 | 大 | 代理维护角色表,做请求级 RBAC |
| 机构/团队管理 | 无机构概念 | 大 | 代理建机构树(总行→分行→支行) + 成员关系 |
| 创建者私有访问 | ✅ permission=me | 无 | 直接复用 |
| 团队共享 | ✅ permission=team | 无 | 直接复用(全租户广播) |
| 机构自动共享 | 无 | 中 | 代理 KB-机构绑定,访问时动态判定 |
| 指定用户共享 | 无 | 中 | 代理 KB-用户 ACL 表 |
| KB 数量上限 | 无 | 中 | 代理在「创建 KB」请求前置校验 |
| 文档上传限额 | 仅全局环境变量 | 中 | 代理按主体维度计数拦截 |
| API 频率限制 | 无 | 中 | 代理层令牌桶/滑动窗口 |
| 模型白名单 | 无 | 中 | 代理拦截 chat/completion 校验 model |
| 版本审批流 | 无(仅 Agent 有 canvas version) | 大 | 代理建审批状态机 + 发布闸门 |
代理服务作为反向代理 + 策略决策点(PDP/PEP)置于客户端与 RAGFlow 之间。所有流量先过代理,鉴权/配额/审批判定后再转发给原生 RAGFlow。
┌────────────┐ ①携带代理签发的Token ┌──────────────────────────────┐
│ 前端 / SDK │ ─────────────────────────► │ 权限代理服务 (Auth Proxy) │
│ 系统应用 │ │ ┌────────────────────────┐ │
└────────────┘ │ │ PEP 拦截器 (Gateway) │ │
│ │ · 身份认证 │ │
┌──────────────────────────────┐ │ │ · 五级RBAC判定 │ │
│ 独立权限数据库 (proxy_db) │◄─┤ │ · KB四模式ACL判定 │ │
│ role / org / kb_acl │ │ │ · 配额/频率校验 │ │
│ quota / approval / audit_log │ │ │ · 模型白名单 │ │
└──────────────────────────────┘ │ │ · 版本审批闸门 │ │
│ └───────────┬────────────┘ │
└──────────────┼───────────────┘
②注入RAGFlow原生Token, 透传
▼
┌──────────────────────────────┐
│ RAGFlow 0.25.6 (原封不动) │
│ api/apps/* + 原数据库 │
└──────────────────────────────┘
网关拦截器,对每个请求做「放行 / 拒绝 / 改写」三态决策。
查 proxy_db 计算有效权限,输出决策,不接触 RAGFlow 库。
用代理身份换取/缓存 RAGFlow 原生 access_token / APIToken。
permission 已被代理正确投影过的 KB。RAGFlow 侧的 accessible() 判定照常运行且永远通过——代理只是提前在外层做了更严格的过滤,从不放宽 RAGFlow 的原生限制。
在代理层定义五级角色,每级是上一级权限的子集。角色存于代理库,与 RAGFlow 的 owner/normal 解耦。
is_superuser=True 的账号。admin/owner。normal 成员且具创建权。normal 成员但代理屏蔽写接口。api_token 路径(g.auth_via_api_token=True)。| 代理角色 | 能力范围 | RAGFlow 原生载体 | 写操作 |
|---|---|---|---|
| 系统管理员 | 全局 | is_superuser 账号 / 专用运维租户 | 全部 |
| 团队管理员 | 本机构 | 租户 role=owner/admin | 本机构全部 |
| 编辑者 | 授权 KB | 租户 role=normal + JWT | 建/编 KB、传文档 |
| 检索用户 | 授权 KB 只读 | 租户 role=normal + JWT | ❌ 代理拦截 |
| 系统应用 | 绑定 KB | api_token | 按配置(通常只读检索) |
normal。区分发生在代理的 PEP 层:检索用户的请求若命中写接口(如 POST /v1/kb/create、/v1/document/upload),代理直接 403,根本不转发给 RAGFlow。
代理在 RAGFlow 的 me|team 二态之上,叠加四种语义化模式。每次 KB 访问由代理先算「有效可见集」,再决定是否转发。
| 模式 | 语义 | 代理实现 | RAGFlow 投影 |
|---|---|---|---|
| ① 创建者直接访问 | 仅创建者可见可改 | kb_acl 默认仅 owner 记录 | permission=me(原生天然支持) |
| ② 机构自动共享 | 本机构及上级机构成员自动可见 支行 KB → 二级分行 → 一级分行 → 总行 逐级可见 |
kb 绑定 org_id;访问者机构匹配则放行 | me + 代理动态授权★ |
| ③ 指定用户共享 | 仅授权名单可见 | kb_acl 显式 (kb_id,user_id) 记录 | me + 代理动态授权★ |
| ④ 团队共享 | 全租户成员可见 | 无需额外 ACL | permission=team(原生天然支持) |
★ 模式②③的关键技巧见下方 6.2「动态授权」。
def visible_kb_ids(subject):
ids = set()
# ① 创建者
ids |= proxy_db.kb.where(created_by == subject.user_id)
# ② 机构自动共享:主体所属机构(含上级分行/总行继承)绑定的 KB
org_ids = org_tree.ancestors_and_self(subject.org_id) # 支行→分行→总行
ids |= proxy_db.kb_share.where(mode=='ORG', org_id in org_ids)
# ③ 指定用户共享
ids |= proxy_db.kb_acl.where(user_id == subject.user_id)
# ④ 团队共享(直接信任 RAGFlow 原生 team)
ids |= proxy_db.kb.where(mode=='TEAM', tenant_id in subject.joined_tenants)
return ids - revoked(subject) # 减去已撤销/审批未通过
team 是全租户广播,无法表达「只给部分人」。若把模式②③也设成 team,会泄露给整个租户。
方案:KB 保持 permission=me(归属"机构服务账号"租户),代理用 Token Broker 动态代理访问。
permission=me,对其他普通用户原生不可见——杜绝越权。
accessible() 自然通过。
me|team 语义。撤销共享 = 删 proxy_db 一条 ACL,即时生效,无需触碰 RAGFlow。
四类配额均在代理 PEP 层「请求前置」校验,超限直接拒绝,不转发。计数维度可按用户/机构/系统应用配置。
拦截 POST /v1/kb/create。校验 count(proxy_db.kb where created_by=subject) < quota.max_kb。超限 403。
拦截 /v1/document/upload。代理自维护 doc 计数(避免依赖 RAGFlow 全局 MAX_FILE_NUM_PER_USER)。支持按 KB / 按主体 / 按文件大小三种限额。
滑动窗口 / 令牌桶(Redis)。Key = service_app_id 或 user_id + 接口组。超频返回 429。专门约束系统应用。
拦截 chat/completion/retrieval 类请求,解析 body 中的 llm_id / model,比对主体白名单。非白名单模型 403。
def enforce_quota(subject, action, payload):
q = proxy_db.quota_policy.resolve(subject) # 角色/机构/个体逐级覆盖
match action:
case "kb.create":
assert proxy_db.count_kb(subject) < q.max_kb, DENY(403)
case "doc.upload":
assert proxy_db.count_doc(subject) + payload.n <= q.max_doc, DENY(403)
assert payload.size <= q.max_file_size, DENY(413)
case "api.call":
assert rate_limiter.allow(subject.id, q.qps), DENY(429)
case "chat.completion":
assert payload.model in q.model_whitelist, DENY(403)
return ALLOW
面向金融合规——KB 内容发布须经审批留痕。代理维护版本状态机;只有「已发布」版本对检索用户/系统应用可见。
| 转换 | 触发者 | 动作 | 代理行为 |
|---|---|---|---|
| — → 草稿 | 编辑者 | 创建/修改 KB 内容 | 所有上传/解析落在「草稿区」标记,检索用户不可见 |
| 草稿 → 待审批 | 编辑者 | 提交审批 | 冻结草稿快照,生成审批单,通知团队管理员 |
| 待审批 → 已发布 | 团队/系统管理员 | 批准 | 打 PUBLISHED 标记;该版本进入检索可见集 |
| 待审批 → 草稿 | 团队/系统管理员 | 驳回 | 退回编辑者,记录驳回意见 |
| 已发布 → 历史 | 系统 | 新版本发布时 | 旧版本归档,保留审计回溯 |
方案:双 KB 影子机制(草稿 KB / 发布 KB)。
{name}__draft 与 {name}__published,均 permission=me 归属服务租户。
全部新增表落在 proxy_db,与 RAGFlow 库物理隔离。仅通过 ragflow_user_id / ragflow_kb_id 等外部主键引用原生实体,不加任何外键约束到 RAGFlow。
| 表 | 核心字段 | 用途 |
|---|---|---|
p_role | id, code(5级), name, permissions(JSON) | 五级角色定义 |
p_org | id, name, parent_id, ragflow_tenant_id | 机构树;绑定服务租户 |
p_user_profile | ragflow_user_id, role_code, org_id | 用户-角色-机构绑定 |
p_kb_meta | ragflow_kb_id, share_mode(4模式), org_id, draft_kb_id, published_kb_id | KB 共享元数据 + 影子映射 |
p_kb_acl | ragflow_kb_id, user_id, grant_type | 指定用户共享名单 |
p_quota_policy | scope(global/role/dept/user), max_kb, max_doc, max_file_size, qps | 分级配额 |
p_model_whitelist | scope_id, llm_ids(JSON) | 模型白名单 |
p_kb_version | ragflow_kb_id, version_no, status(草稿/待审批/已发布/历史), snapshot | 版本状态机 |
p_approval | version_id, submitter, approver, decision, comment, ts | 审批单 |
p_service_app | id, api_key, ragflow_api_token, bound_kb_ids, qps, model_whitelist | 系统应用注册 |
p_audit_log | actor, action, target, before/after, ts | 全量审计 |
p_token_broker | tenant_id, access_token, expire_at | 缓存原生 Token |
单个请求在代理内的完整生命周期——这是整套设计的执行主干。
p_user_profile / p_service_app,确定主体 + 角色 + 机构。
enforce_quota。超限 403/413/429。
p_audit_log,回传客户端。
把细粒度模型「投影」到 RAGFlow 二态权限的完整对照——这是「零侵入」成立的技术保证。
| 代理概念 | RAGFlow 原生载体 | 是否新增 RAGFlow 数据 |
|---|---|---|
| 5 级角色 | 租户角色(owner/normal) + api_token,差异化由代理 PEP 补 | 否(沿用原表) |
| 机构 | 「机构服务租户」(一个普通 tenant) | 否(用原 tenant 注册流程) |
| 创建者私有(模式①) | permission=me | 否 |
| 机构共享(模式②) | me + 服务租户 Token 代理 | 否 |
| 指定用户(模式③) | me + ACL + Token 代理 | 否 |
| 团队共享(模式④) | permission=team | 否 |
| 版本审批 | draft/published 双影子 KB | 否(用原 KB+文档接口) |
| 系统应用 | api_token 表 | 否(用原 API Key 发放) |
| 配额/频率/白名单 | —(纯代理层,RAGFlow 无感) | 否 |
me KB 或 team KB 的标准请求。它的 accessible()、_visibility_and_status_filter()、_load_user() 全部按原样运行且永远得到一致结果。所有「拒绝」都发生在请求到达 RAGFlow 之前。
| 阶段 | 交付 | 关键风险点 |
|---|---|---|
| P0 地基 | 代理网关骨架 + Token Broker + proxy_db 建表 + 透传打通 | 原生 Token 生命周期/刷新 |
| P1 角色与 RBAC | 五级角色、路由→动作映射、写操作拦截 | 接口清单覆盖完整性 |
| P2 KB 四模式 | 机构树、kb_acl、有效可见集、列表二次过滤 | 服务租户代理访问的响应裁剪 |
| P3 配额管控 | 分级配额、Redis 频率、模型白名单 | 计数一致性(并发上传) |
| P4 版本审批 | 双影子 KB、状态机、审批单、审计 | draft→published 文档同步原子性 |
| P5 合规加固 | 全量审计、四眼原则、回溯报表 | 金融审计字段完整性 |
_load_user 双令牌逻辑、② KB accessible 语义、③ 文档/检索接口契约 三处是否变动。这三点是代理唯一的耦合面,验证成本极低。