企业协同办公

Odoo 企业版 Knowledge 为什么不是“父文章一改权限全树跟着变”而已:desync 脱同步、成员继承与最后一名 writer 约束讲透

很多人把 Odoo 企业版 Knowledge 的权限理解成“根文章设 read/write/none,子文章原样继承”。但 `knowledge` 源码真正做的是一套可局部脱同步的权限树:子节点可以在降权时复制父级成员后切断继承,根/子节点还始终要满足至少一名 writer,移动到 private/shared 时也会重建权限边界。

企业 协同办公
进阶 开发者 3 分钟阅读
0 评论 0 点赞 0 收藏 6 阅读

先说结论

很多人第一次看 Odoo 企业版 Knowledge,会把权限模型想得很线性:

  • 根文章设置 internal_permission
  • 子文章自动继承
  • 成员权限不过是额外加几个人
  • 改完父节点,整棵树一起刷新

这个理解只够描述 UI,不够解释源码。

/home/ubuntu/odoo-temp/enterprise/knowledge 里的 models/knowledge_article.pyknowledge_article_member.py 以及权限测试来看,Knowledge 真正实现的不是一棵“永远全量继承”的权限树,而是一棵 允许局部脱同步(desynchronize) 的协作树:

  1. 子文章平时继承父级权限,但可以在降权时复制父级访问并切断继承;
  2. 成员权限不是简单追加,而是和 inherited permission 合并计算当前用户权限;
  3. 无论怎么改,文章都必须始终保留至少一名 writer;
  4. 移动到 private / shared / workspace,不是换个标签,而是重建权限边界;
  5. 脱同步节点之后再新增父级成员,不会继续往下污染这条子树。

Knowledge 的权限核心不是“继承”,而是“默认继承 + 必要时复制一份并断开”,从而让知识树既能批量治理,又能做局部例外。

这也是它比很多普通 wiki 权限模型更成熟的地方。


一、internal_permission 不是全部权限,它只是内部用户的默认底座

knowledge.article 上最显眼的字段当然是:

  • internal_permission: write / read / none
  • article_member_ids: 显式成员表
  • inherited_permission
  • user_permission
  • is_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 降成 readnone,原本依赖父级继承拿到访问的人,权限就会一下子塌掉。

而 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 不是当前文章自己、
  • 且确实来自父层继承的成员,

才会在脱同步时被落成本地成员。

所以脱同步不是“把父文章数据复制一遍”,而是:

把这篇文章当前真正吃到的上游权限快照成本地配置。

这能避免两个常见问题:

  1. 只复制直接父节点,漏掉更高层祖先带来的有效权限;
  2. 连本地已有成员一起重复复制,造成脏配置。

五、为什么 Knowledge 死活要保证“至少一名 writer”

如果只看协作体验,很多人可能觉得:

  • 全部设成只读也没什么;
  • 或者把最后一个编辑者删掉,等需要时再加回来。

Knowledge 不这么想。

knowledge.articleknowledge.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 改成 readnone,系统会先:

  • self._add_members(self.env.user.partner_id, 'write')

也就是先把操作者补成显式 write member。

官方注释说得很明确:这是为了 确保修改后当前用户仍然有 write access

这件事看起来像小细节,实际上在防止一种很糟糕的交互事故:

  • 你正在整理权限;
  • 一次降权操作把自己也一起踢出了编辑权;
  • 结果你刚设完规则,就再也没法继续修;
  • 最后只能找管理员救火。

Knowledge 的处理方式很“企业软件”:

  • 权限可以收紧;
  • 但当前正在执行收紧动作的人,不能因为一次操作立即失去治理能力。

所以它优先保护的是 权限调整过程的可完成性


七、private / shared / workspace 不是显示分类,而是权限结果的投影

category 字段有三个值:

  • workspace
  • private
  • shared

如果只看左侧栏,容易以为这只是 UI 分区。

_compute_category() 表明,它其实是权限结构投影出来的结果:

  • 根文章 internal_permission != 'none',就是 workspace
  • 根文章 internal_permission == 'none' 且存在非 none 成员,则归 shared
  • 否则就是 private

换句话说:

  • workspace = 对内部员工开放;
  • shared = 不对内部全员开放,但通过成员共享给多人;
  • private = 更私有,通常只剩极少成员甚至单人。

所以当你把文章“移到 private / shared”时,系统不是改一个分类字段给菜单看。

它做的是重新布置:

  • internal_permission
  • article_member_ids
  • is_desynchronized
  • 父子层级

这就是为什么 _move_and_make_private()_move_and_make_shared_root() 代码会这么重。


八、把文章改成 private 时,Knowledge 先拆掉你管不了的后代

_move_and_make_private() 里最值得注意的一步,不是删成员,而是先调用:

  • _detach_unwritable_descendants()

官方考虑的是:

  • 你能改当前文章,
  • 不代表你有权重写整棵子树里每个后代的权限。

所以对那些你 没有 write 权限 的后代,Knowledge 不会粗暴跟着一起改私有,而是:

  1. 先把同步节点可继承的父级权限复制到本地;
  2. 把这些无权处理的后代从原层级中 detach 成 root
  3. 同时重置 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

评论区

想参与讨论?先 登录 再发表评论。
还没有评论,你可以成为第一个留言的人。