先说结论
很多人第一次看 Odoo 企业版 Knowledge,会把权限模型想得很线性:
- 根文章设置
internal_permission - 子文章自动继承
- 成员权限不过是额外加几个人
- 改完父节点,整棵树一起刷新
这个理解只够描述 UI,不够解释源码。
从 /home/ubuntu/odoo-temp/enterprise/knowledge 里的 models/knowledge_article.py、knowledge_article_member.py 以及权限测试来看,Knowledge 真正实现的不是一棵“永远全量继承”的权限树,而是一棵 允许局部脱同步(desynchronize) 的协作树:
- 子文章平时继承父级权限,但可以在降权时复制父级访问并切断继承;
- 成员权限不是简单追加,而是和 inherited permission 合并计算当前用户权限;
- 无论怎么改,文章都必须始终保留至少一名 writer;
- 移动到 private / shared / workspace,不是换个标签,而是重建权限边界;
- 脱同步节点之后再新增父级成员,不会继续往下污染这条子树。
Knowledge 的权限核心不是“继承”,而是“默认继承 + 必要时复制一份并断开”,从而让知识树既能批量治理,又能做局部例外。
这也是它比很多普通 wiki 权限模型更成熟的地方。
一、internal_permission 不是全部权限,它只是内部用户的默认底座
knowledge.article 上最显眼的字段当然是:
internal_permission:write/read/nonearticle_member_ids: 显式成员表inherited_permissionuser_permissionis_desynchronized
很多人会把 internal_permission 直接当作“最终权限”。
但 _compute_user_permission() 的实现说明,真正给当前用户落地的是 合并后的 user_permission:
- 内部用户:结合 内部默认权限 + 显式成员权限;
- 外部 / share 用户:不吃 internal_permission,只看 member;
- public 用户:直接没有权限。
也就是说:
internal_permission更像“对所有内部员工的默认开放级别”;article_member_ids才是“对具体伙伴的例外规则”;user_permission则是两者融合后的最终结果。
这解释了为什么 Knowledge 同时需要“全员内部可读/可写”与“指定成员 read/write/none”两套模型。
因为它要同时服务:
- 内部知识空间;
- 少量定向共享;
- 对某些人做覆盖;
- 私有文章与共享文章的混合层级。
二、子文章继承权限时,不是一直爬到根,而是遇到“脱同步节点”就停
_compute_inherited_permission() 这一段是理解整套设计的关键。
源码逻辑不是无脑向上追溯根节点,而是:
- 如果当前文章自己设置了
internal_permission,那继承权限就等于自己; - 否则沿父链向上找;
- 一旦遇到 有
internal_permission的祖先,就在那里停; - 如果途中遇到
is_desynchronized=True的祖先,也立刻停。
这意味着 is_desynchronized 在 Knowledge 里不是装饰字段,而是 权限树的断点标记。
它告诉系统:
从这里开始,这个节点不再继续跟着更高层父节点的权限变化走。
所以脱同步的本质不是“把某字段改一下”,而是把原本的一条继承链,变成一个新的局部权限根。
这正是企业知识库里最常见、也最难处理的场景:
- 大多数文章应该沿部门/空间规则统一治理;
- 少数敏感页、专项页、交接页,需要保留特殊成员和更细粒度权限;
- 但你又不想为此彻底打散整棵树。
Knowledge 选的解法就是:默认继承,例外脱同步。
三、为什么“降权”会触发脱同步,而“升权”往往只是补一个成员
set_internal_permission() 和 set_member_permission() 两个方法把这个策略写得非常直白。
1. 改内部权限:降权时可能脱同步
set_internal_permission() 会先判断:
- 当前文章是否还在同步状态;
- 是否有父节点;
- 父节点继承权限是否高于你要改成的新权限。
如果答案是“是”,就进入 downgrade 分支:
- 调用
_desync_access_from_parents_values(); - 把父级当前可见的访问规则复制下来;
- 把当前文章记成
is_desynchronized=True; - 再把自己的内部权限强制成新的较低权限。
这一步的意义非常大。
如果不先复制父级成员再断链,你一旦把子文章从父级 write 降成 read 或 none,原本依赖父级继承拿到访问的人,权限就会一下子塌掉。
而 Odoo 的做法是:
- 先把你现在实际继承到的成员状态“落盘”;
- 再从父链里切出去;
- 之后你就可以在自己的局部树里慢慢改。
这是一种很典型的 copy-on-write 权限分叉。
2. 改成员权限:降 inherited member 也会脱同步
set_member_permission() 也一样。
如果你改的是当前文章本地成员,直接更新即可; 但如果你改的是 来自父级继承的成员:
- 提权时,Knowledge 通常直接在当前文章补一个更高权限成员;
- 降权时,则会触发脱同步,把父级成员复制为本地成员后再降级。
测试 test_set_member_permission() 明确验证了这个边界:
- inherited member 从父级继承
write时,子文章降成read会变成 desync; - 之后该子树的后代继续继承这个被降级后的本地规则;
- 父级后续再加新成员,也不会自动进入这条已脱同步的子树。
这说明 Odoo 在这里防的是“上游继续污染下游例外节点”。
四、_copy_access_from_parents_commands() 做的不是简单复制,而是复制“当前实际生效的继承访问”
Knowledge 的高明之处在于,它脱同步时并不是粗暴地把父文章的 article_member_ids 原样拷下来。
真正使用的是:
_desync_access_from_parents_values()_copy_access_from_parents_commands()_get_article_member_permissions()
这里复制的对象是 当前文章实际拿到的成员权限视图,而不是单一父节点表面存的那几条 member 记录。
这意味着它复制的是:
- 已综合祖先继承链之后,
- 对当前文章真实有效的成员权限结果。
然后只有那些:
based_on不是当前文章自己、- 且确实来自父层继承的成员,
才会在脱同步时被落成本地成员。
所以脱同步不是“把父文章数据复制一遍”,而是:
把这篇文章当前真正吃到的上游权限快照成本地配置。
这能避免两个常见问题:
- 只复制直接父节点,漏掉更高层祖先带来的有效权限;
- 连本地已有成员一起重复复制,造成脏配置。
五、为什么 Knowledge 死活要保证“至少一名 writer”
如果只看协作体验,很多人可能觉得:
- 全部设成只读也没什么;
- 或者把最后一个编辑者删掉,等需要时再加回来。
Knowledge 不这么想。
knowledge.article 和 knowledge.article.member 两层都实现了“必须至少有一个 writer”的约束:
- 文章级
_check_is_writable(); - 成员级
_check_is_writable(on_unlink=False/True); - 删除成员时还有
_unlink_except_no_writer()补检查。
判断规则并不只是“文章自己有没有 write member”,而是:
- 如果继承权限本身就是
write,那可以; - 否则必须能在本地或可继承成员里找到至少一个
write; - 删除成员前,也要预判删掉之后会不会把最后 writer 干掉。
这套约束说明官方把文章看成一个必须保持可治理状态的协作对象。
Knowledge 可以收紧权限,但不能把文章锁成一个谁都改不了、也没人负责的死对象。
这在企业协作里很合理。
因为知识库不是档案馆,它通常还承担:
- 规范维护;
- 轮值更新;
- 交接修订;
- FAQ 持续演进。
如果允许出现“没有 writer 的文章”,很多页面最终会变成无人维护的孤岛。
六、为什么降内部权限时,系统会先把当前操作者补成 write member
set_internal_permission() 还有一个很细但非常产品化的判断:
如果当前用户本来有写权限,而你又准备把文章从 write 改成 read 或 none,系统会先:
self._add_members(self.env.user.partner_id, 'write')
也就是先把操作者补成显式 write member。
官方注释说得很明确:这是为了 确保修改后当前用户仍然有 write access。
这件事看起来像小细节,实际上在防止一种很糟糕的交互事故:
- 你正在整理权限;
- 一次降权操作把自己也一起踢出了编辑权;
- 结果你刚设完规则,就再也没法继续修;
- 最后只能找管理员救火。
Knowledge 的处理方式很“企业软件”:
- 权限可以收紧;
- 但当前正在执行收紧动作的人,不能因为一次操作立即失去治理能力。
所以它优先保护的是 权限调整过程的可完成性。
七、private / shared / workspace 不是显示分类,而是权限结果的投影
category 字段有三个值:
workspaceprivateshared
如果只看左侧栏,容易以为这只是 UI 分区。
但 _compute_category() 表明,它其实是权限结构投影出来的结果:
- 根文章
internal_permission != 'none',就是workspace; - 根文章
internal_permission == 'none'且存在非 none 成员,则归shared; - 否则就是
private。
换句话说:
- workspace = 对内部员工开放;
- shared = 不对内部全员开放,但通过成员共享给多人;
- private = 更私有,通常只剩极少成员甚至单人。
所以当你把文章“移到 private / shared”时,系统不是改一个分类字段给菜单看。
它做的是重新布置:
internal_permissionarticle_member_idsis_desynchronized- 父子层级
这就是为什么 _move_and_make_private()、_move_and_make_shared_root() 代码会这么重。
八、把文章改成 private 时,Knowledge 先拆掉你管不了的后代
_move_and_make_private() 里最值得注意的一步,不是删成员,而是先调用:
_detach_unwritable_descendants()
官方考虑的是:
- 你能改当前文章,
- 不代表你有权重写整棵子树里每个后代的权限。
所以对那些你 没有 write 权限 的后代,Knowledge 不会粗暴跟着一起改私有,而是:
- 先把同步节点可继承的父级权限复制到本地;
- 把这些无权处理的后代从原层级中 detach 成 root;
- 同时重置
is_desynchronized=False,因为 root 文章不能处于 desync 状态。
test_article_make_private_w_desynchronized() 还专门验证了一个很细的点:
- 如果某个孩子原本已经 desync,
- 那么父文章 later 新增的成员,不应该在 detach 时又被错误复制进去。
这说明 Odoo 不只是“会拆树”,还很在意拆树时 不要把不该继承的成员重新污染回来。
九、为什么移动到 shared root 需要至少 2 个成员
_move_and_make_shared_root() 里还有个很企业化的约束:
- 如果总成员数不足 2,就抛
You need at least 2 members for the Article to be shared.
这背后的定义很明确:
shared 不是“单人私有但我换个分类名”,而是至少涉及两方协作对象。
同时,这个方法也会:
- 收集当前文章实际有效的成员权限;
- 把那些原来只是继承到的成员,落到新 shared root 上;
- 如果当前操作者原来只是靠 internal_permission 才有写权,还会把他显式加入为 write member。
也就是说,shared root 不是一个“视觉上脱离原树”的动作, 而是一次 把协作关系显式化 的动作。
十、这套设计最值得借鉴的 4 个点
如果把 Knowledge 的权限机制抽象一下,我觉得最值得借鉴的是四件事。
1. 默认继承,例外节点 copy-on-write
大部分节点轻装上阵, 只有出现局部降权 / 局部例外时,才复制父级有效权限并切断继承。
这比“每个节点都存完整 ACL”轻得多,也比“永远只能继承”灵活得多。
2. 权限调整时先保护当前操作者
改权限的人不能因为一次降权把自己也锁死。 这是治理体验的底线。
3. 始终保证至少一个 writer
协作文档不是死档案,必须保留维护责任人。
4. 移动分类时不只改标签,而是重建治理边界
private / shared / workspace 是权限拓扑变化,不是 UI 分组变化。
最后总结
如果只把 Odoo 企业版 Knowledge 理解成“带层级的 wiki”,会低估很多源码设计。
它真正难的部分,不是编辑器,也不是树结构本身,而是:
- 如何让大多数页面沿父级统一治理;
- 又允许某些子页面安全地做局部例外;
- 还要防止权限修改把文章搞成无人可写;
- 在 private / shared / workspace 之间迁移时,继续保住协作语义。
所以更准确的总结是:
Knowledge 的权限模型,本质上是一套“继承优先、脱同步兜底、writer 永不归零”的企业知识协作治理机制。
这也是为什么它不是一个简单的树形页面系统,而是一个真正能承载企业知识库治理的协同模块。
DISCUSSION
评论区