跳转到内容

RBAC 与多租户隔离

RBAC 与多租户隔离

学习目标

本章聚焦 LibreChat 的权限管理系统。你将了解:

  • accessControlService 的统一权限入口
  • 管理员模块的角色、用户、组管理
  • preAuthTenant 中间件的租户解析
  • 资源级 ACL 的执行流程

前置知识

  • RBAC 基础概念
  • Express 中间件机制

项目实践

统一权限服务

accessControlService 是所有权限检查的统一入口:

accessControlService:
├── hasPermission(userId, resource, action) → boolean
├── getResourceAccess(userId, resourceId) → AccessLevel
├── getTenantIsolation(userId, tenantId) → boolean
└── checkResourceOwnership(userId, resourceId) → boolean

所有需要权限检查的代码路径都通过这个服务,而非直接查询数据库或检查字段。

管理员模块

packages/api/src/admin/ 目录包含完整的管理功能:

文件职责
config.ts系统级配置管理
users.ts用户 CRUD
roles.ts角色定义与分配
grants.ts权限授予与回收
groups.ts用户组管理
pagination.ts通用分页逻辑

租户隔离中间件

preAuthTenant 中间件在认证前解析租户上下文:

preAuthTenant(req, res, next):
1. 从请求中提取租户标识(域名、header、subdomain)
2. 查询租户配置
3. 将 tenantId 挂载到 req 对象
4. next()

为什么在认证前:某些场景需要根据租户决定认证策略(如 SSO 提供商选择),因此租户解析先于认证。

资源级 ACL 执行流程

以 Skills 系统为例:

1. resolveAgentScopedSkillIds(agent, accessibleSkillIds, ...)
→ 过滤出该 Agent 有权使用的技能 ID
2. resolveSkillActive(skill, skillStates, userId, ...)
→ 判断该用户对该技能的活跃状态
3. injectSkillCatalog(agent, accessibleSkillIds, ...)
→ 将活跃技能注入 Agent 的 additional_instructions
4. getSkillByName(name, accessibleIds, ...)
→ 运行时解析技能,约束到 ACL 范围内

每一层都执行权限检查,而非仅在入口检查一次。这种纵深防御确保即使某一层绕过,其他层仍能拦截。


问题与规避

问题 1:跨租户数据泄露

规避:所有数据库查询必须携带 tenant 过滤条件。MongoDB 的 schema 中包含 tenantId 字段,Mongoose 的 query middleware 自动注入 tenant 过滤。

问题 2:权限检查在多处重复实现

规避:统一通过 accessControlService 入口。新增权限类型时只需扩展此服务,所有调用方自动受益。

问题 3:租户解析失败导致错误的安全上下文

规避preAuthTenant 失败时设置默认租户(而非跳过),确保每个请求都有明确的租户标识。后续中间件依赖此标识执行过滤。


设计取舍

纵深防御 vs 单一入口检查

方案优势代价
纵深防御(LibreChat)多层保护、绕过风险低每层都需要权限逻辑、维护成本高
单一入口简单、集中管理入口被绕过即完全失效

LibreChat 选择了纵深防御,因为 Skills、Agents、Prompts 等资源在不同代码路径中被访问,单一入口无法覆盖所有场景。


参考来源