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 等资源在不同代码路径中被访问,单一入口无法覆盖所有场景。