先说结论
Odoo 企业版 Documents 的“分享链接”绝不是“生成一个 URL 发出去”这么简单。
从 /home/ubuntu/odoo-temp/enterprise/documents/models/documents_document.py 和 views/documents_templates_share.xml 看,真正决定访问边界的至少有 5 层:
document_token/access_tokenaccess_url- 当前文档本身的
user_permission access_via_link是否开放查看或编辑- 父文件夹是否可达,以及模板是否据此暴露上传/下载入口
所以外部人能否访问,不是一个“有链 / 没链”的问题,而是:
这个 token 能把人带到哪里;到了那里以后,系统还认不认他对当前文档或父文件夹有权。
第一层:access URL 只是入口,不是最终授权结果
源码里 _compute_access_token() 会把 document_token 和记录 id 组合成一个 access_token,然后 _compute_access_url() 再生成类似:
/odoo/documents/<access_token>
这说明分享链接本质上只是一个可定位入口。
它解决的是:
- 用户不用先进入内部 web client 再找文档
- 系统能够凭 token 找到目标文档或文件夹
但它并没有承诺“有链接就一定有权限”。
这一点从分享页模板的兜底提示也能看出来:
This document does not exist or is not publicly available.
也就是说,链接只负责“把请求导到正确对象”,真正是否可读,还要继续过权限判断。
第二层:Documents 的权限不是单点判断,而是来源叠加
_compute_user_permission() 这一段是整篇文章的核心。
它先调用 _get_permission_without_token_multi() 计算“不考虑 token 时,当前用户本来拥有什么权限”,然后再叠加 link-based access 等特殊情况。
权限来源大致有几层:
1. 系统管理员 / Documents 管理组
如果用户属于 documents.group_documents_system,并且公司上下文允许,权限几乎直接给到 edit。
2. 文档 owner
如果文档 owner_id == 当前用户,默认能编辑。
3. 明确配置的 documents.access
代码会按 (document_id, partner_id) 找有效 access 记录,并结合 expiration date 判断是否仍生效。
4. internal / via link / folder 继承
如果文档本身不给权限,系统还会看:
- 当前文档是否开放
access_via_link - 父文件夹是否可访问
- 当前对象是不是 shortcut
- link access 是否被隐藏
所以一个用户能看到文档,不一定是因为他“拥有这份文档”,也可能是因为:
- 他是 owner
- 他被单独授权了
- 他能通过父文件夹进入
- 他拿着一个具备 link-access 语义的入口
第三层:为什么“有分享链接却还是打不开”
源码里有一段非常关键的边界:
- 如果
document.user_permission == 'none' - 但它有
folder_id - 且
access_via_link != 'none' - 且 link access 没被隐藏
- 且用户对父文件夹仍可达
那么系统才可能把当前文档权限提升到 document.access_via_link。
这几乎等于在说:
链接权限不是凭空生效,而是模拟“你能顺着界面路径走到这里”的一层宽松入口。
并且注释里还专门写了:
- 这只按“上一层父文件夹”来模拟
- 不是无限级穿透
所以典型失败场景是:
- 你发的是子文件或子文件夹链接
- 但父级访问关系不成立
- 系统就不会把 link access 无条件放大
这正是很多人误会的地方:Documents 的外链不是对象级万能通行证,它更像是受上下文约束的外部门把手。
第四层:为什么有的分享页能上传,有的只能下载
看 documents_templates_share.xml,分享页是否显示 Upload 按钮,条件写得非常直白:
folder.access_via_link == 'edit'- 或
folder.user_permission == 'edit'
也就是说,上传能力只在两种情况下开放:
- 这个分享的文件夹本身允许 link-based edit
- 当前访问者对这个文件夹本来就有 edit 权限
否则即使能打开分享页,也可能只有 Download All 或只读浏览。
这很好地解释了现场一个很常见的误解:
客户已经能看到共享文件夹了,为什么还是不能回传文件?
因为“能看”与“能上传”是两档权限,不是同一个开关。
第五层:为什么子文件夹和文件的行为看起来不完全一致
分享模板里对子文件夹、二进制文件、URL 文档做了不同渲染:
- 子文件夹用自己的
access_token跳转 - binary 文档会看有没有 attachment / thumbnail / download 能力
- requested document 在可编辑时会显示上传入口
这说明 Documents 的分享不是单一对象页面,而是一个按对象类型和当前权限动态降级的公共入口。
同样是“一个分享文件夹里面的内容”,不同项可能出现:
- 能下载
- 能预览
- 只能看名字
- 需要补传文件
- 能继续下钻到子文件夹
所以用户眼中的“不一致”,其实是模板把底层权限差异直接显露出来了。
第六层:root owner 与 share user 的限制,为什么很重要
_check_root_documents_owner_id() 会拦一类看似很细、其实很关键的错误:
- root documents / folders 不能随便给 share user 当 owner
源码通过 _get_unauthorized_root_document_owners_sudo() 去筛掉这类 owner。
这背后的设计边界很明确:
- 外部共享用户可以作为访问参与者
- 但不应天然成为整棵根级文档树的所有者
这能避免 Documents 的“共享”演变成“外部人拥有内部根目录”的权限污染。
第七层:实施时最容易踩的 4 个坑
1. 把 token 当授权本身
错。token 更像定位入口,不是万能授权。
2. 忽略父文件夹可达性
如果你的分享模型依赖 folder 继承,那父级不可达时,子项 link access 也可能失效。
3. 误以为 view = upload
分享页能看,并不等于能上传;上传要求 edit。
4. 把外部共享当内部协作
Documents 很适合做受控外链协作,但它不是“随便给链接就和内部同权”的设计。
最后一句
Odoo 企业版 Documents 的分享边界,真正值得理解的是这条顺序:
- 先凭 token 找到对象
- 再根据 owner / access record / folder / link rule 算 permission
- 最后由分享模板决定给用户露出哪些动作
所以外链分享从来不是“一个链接搞定权限”,而是:
链接负责到达,权限负责可见,模板负责暴露动作。
把这三层分开看,Documents 的行为就不再神秘。
DISCUSSION
评论区