先说结论
Odoo 的 LDAP 集成并不是“把账号密码丢给目录服务器验证一下”这么简单。
从 /home/ubuntu/odoo-temp/addons/auth_ldap/models/res_users.py 和 res_company_ldap.py 看,真正影响成败的有四个关键点:
- Odoo 先尝试本地登录,再尝试 LDAP。
- LDAP 过滤器必须最终只命中 1 条目录记录。
- 本地不存在用户时,是否自动创建、复制谁的权限,决定了首登后的权限轮廓。
- 如果密码是在 LDAP 里改的,Odoo 会把本地密码清空,避免两套密码长期并存。
所以,LDAP 集成的本质不是“接入一个认证源”,而是把目录身份映射成 Odoo 用户生命周期。这一步没想清楚,后面就会出现:有人登不进来、有人权限过大、有人改密后行为异常。
Odoo 的登录顺序:先本地,后 LDAP
res.users._login() 的逻辑非常值得注意。
Odoo 会先执行父类的本地登录;只有本地登录抛出 AccessDenied 后,才会继续看 LDAP。
而且它不是一失败就立刻走 LDAP,而是先查数据库里是否已经存在同名本地用户:
- 如果本地已有这个 login,并且本地认证失败,Odoo 直接报错,不再拿同名账号去 LDAP 试。
- 如果本地没有这个 login,才会遍历 LDAP 配置,尝试
_authenticate()。
这意味着什么?
这意味着“同名账号优先本地”
很多管理员以为:
- “我已经接了 LDAP,以后所有同名用户都会走域控认证。”
其实不完全对。
如果数据库里已经有一个 alice@example.com 的本地用户,且它不是通过 LDAP 首登自动建出来的那个流程承接的,那么这次登录失败后,Odoo 不会再帮你去 LDAP 补验一次。
这是一条非常明确的边界:
- LDAP 主要负责“本地还不存在该用户”的首次接入;
- 一旦本地已有同 login 账号,登录优先级会回到本地账户语义。
所以在正式上线前,清理历史测试账号、统一 login 格式,非常重要。
真正的入口不是服务器地址,而是 LDAP filter
res.company.ldap 里最关键的字段不是 ldap_server,而是 ldap_filter。
源码里明确写了几点:
- 过滤器字符串里应该包含
%s占位符; - 用户输入的 login 会替换进去;
- 最终查询结果必须刚好 1 条;
- 如果返回 0 条,或者返回多条,都视为无效登录。
这是很多项目最容易踩的坑。
为什么“一条且仅一条”这么重要?
因为 Odoo 不是在做模糊搜索,它是在做身份决议。
如果你写成:
(|(mail=%s)(uid=%s))
那就必须确保:
- 邮箱和 uid 不会把两个不同的人都匹配出来;
- 历史离职账号、别名账号、测试账号不会形成重复命中;
- 大小写、空格、前后缀被统一处理。
源码里 _get_entry() 会在 _query() 后,把没有 DN 的结果过滤掉,然后只接受长度为 1 的结果。也就是说,哪怕目录服务器返回两条合法记录,Odoo 也会直接视为失败。
所以 LDAP 登录稳定性的第一原则不是“连得上”,而是“过滤器能稳定唯一定位人”。
TLS 不是装饰项,开了就要求服务端真支持 STARTTLS
ldap_tls 字段的帮助文本写得很直白:这不是“更安全一点”的可选开关,而是会在 _connect() 里直接执行 connection.start_tls_s()。
这就意味着:
- 目录服务支持 STARTTLS,开它是对的;
- 目录服务不支持 STARTTLS,开它会导致所有认证都失败。
很多团队把这一步当成浏览器里“允许 HTTPS”那种宽松概念,结果线上发现所有人突然登不进来。
更准确的理解应该是:
ldap://host:port+start_tls_s()是一种明确的连接协商;- 它不是自动降级;
- 失败了就是失败,不会偷偷回落到明文。
所以,LDAP 联调里真正要验证的是:
- 端口是否通;
- binddn 是否可用;
- filter 是否唯一命中;
- STARTTLS 能否真实握手成功。
缺任何一个,最后的“登录失败”在用户视角里都会长得一模一样。
自动建用户时,Template User 决定的是“复制哪种组织角色”
_get_or_create_user() 是这个模块最有价值的地方。
如果本地没有这个 login,而且 create_user=True,Odoo 会:
- 根据 LDAP 条目映射出一组基础值;
- 至少写入
name、login、company_id; - 如果 login 看起来像单邮箱,还会顺手写
email; - 如果配置了
user(也就是 Template User),就调用copy(default=values); - 否则直接
create(values)。
这段逻辑揭示了一个现实:
Odoo 并不会从 LDAP 组自动推导出完整权限模型
开源标准模块这里做的事情非常克制:
- 认证来源来自 LDAP;
- 本地用户档案仍然是 Odoo 用户;
- 权限主要依赖 Odoo 侧复制或默认创建。
因此 Template User 不是一个“可有可无的样板账号”,而是首登权限模板。
如果你把它设置成:
- 管理员账号:首批 LDAP 用户可能全变高权;
- 普通内勤账号:销售、会计、仓库的人首登后都权限不足;
- 多公司错位账号:新用户 company_id 虽按 LDAP 配置写入,但复制出来的可见公司、组、默认值可能仍然带偏。
更稳妥的做法通常是:
- 准备一个最小权限的 Template User;
- 把“谁能进入系统”与“进入后拥有什么角色”拆开治理;
- 复杂授权继续在 Odoo 内通过用户组、自动化规则或 HR 流程处理。
create_user=False 时,LDAP 只是认证器,不负责开户
如果 create_user=False,而本地又没有该用户,源码会抛出:
No local user found for LDAP login and not configured to create one
这条报错背后的设计很清晰:
- LDAP 可以负责验明身份;
- 但是否允许这个人进入当前 Odoo 数据库,是本地系统自己的决定。
这在以下场景非常有用:
- 你只允许已审批员工登录;
- 你希望工单账号、外包账号、临时账号不能自动开户;
- 你有严格的许可证或岗位控制,不想让“能过域控的人”自动占用 Odoo 用户席位。
所以,create_user 的真实含义不是“偷懒开关”,而是身份验证和账户开通是否绑定。
已有本地用户时,_check_credentials 会给 LDAP 第二次机会
登录阶段的 _login() 关注的是“这个 login 能不能进来”。
而 _check_credentials() 关注的是“当前这个已知用户,给的密码是否有效”。
源码逻辑是:
- 先走父类本地密码检查;
- 如果失败,且本次凭据类型是
password; - 且当前用户允许用密码而不是仅 API key;
- 且用户仍处于激活状态;
- 那么再遍历 LDAP 配置,用当前用户的 login 去目录里认证。
这说明 Odoo 对 LDAP 的支持并非只限于“首登自动开户”,也覆盖了已存在用户的后续密码校验。
但这里仍然有边界:
- 这依赖当前
self.env.user.login; - 只在密码型凭据场景有效;
- 如果系统被配置为 API key only,就不会允许密码兜底。
也就是说,LDAP 在 Odoo 中是被嵌进既有认证框架里的,不是完全替代整个认证栈。
LDAP 改密码成功后,为什么 Odoo 要把本地密码清空?
这是很多人第一次看源码时会觉得“很奇怪”的地方。
change_password() 如果通过某个 LDAP 配置成功执行了 _change_password(),紧接着就会调用 _set_empty_password(),直接把 res_users.password 更新成 NULL。
这样做不是多余,而是为了避免两套密码状态漂移。
不清空会有什么问题?
如果用户在 LDAP 改了密码,但 Odoo 还保留一份旧的本地哈希,就会出现一堆灰区:
- 某些入口可能仍然接受旧本地密码;
- 管理员误以为“LDAP 改密已经全局生效”,实际部分请求还在吃旧口令;
- 用户自己也会混淆:到底该记哪套密码?
源码这里的策略非常干脆:
- 密码权威源既然是 LDAP,Odoo 本地就不要留可用副本。
这是一条非常成熟的设计边界。
LDAP 改密不是“所有目录都一定支持”
_change_password() 的实现是真去做:
- 先根据 login 找到唯一 DN;
- 用旧密码绑定;
- 调
passwd_s(dn, old_passwd, new_passwd)。
这意味着要成功,前提包括:
- 目录允许用户自助改密;
- 当前绑定方式具备该能力;
- TLS / 安全策略满足要求;
- 目录本身支持此操作语义。
如果不满足,Odoo 会继续回到父类的本地改密流程。
所以管理员要想清楚:
- 你是要“统一由 LDAP 管密码”,还是
- “LDAP 只负责登录,Odoo 本地仍保留部分密码行为”。
两种都能跑,但治理逻辑完全不同。
配置建议:上线前至少做这 6 个检查
如果你准备把 auth_ldap 用到正式环境,我建议至少做以下检查:
1. 统一 login 口径
- 是邮箱登录,还是工号/uid 登录?
- Odoo 现有账号是否已经统一成同一格式?
- 大小写、空格、历史别名是否已清理?
2. 用真实样本验证 filter 唯一性
不要只挑一个管理员账号测成功。
至少测:
- 普通员工
- 同名/近似名员工
- 离职或停用账号
- 有邮箱别名的账号
3. 明确 Template User 的权限边界
它最好不是管理员,也不是权限过于贫瘠的空白号。
理想状态是:
- 能安全首登;
- 不会越权;
- 后续再按部门补组。
4. 决定是否允许自动开户
- 大企业、强审批流程:更适合
create_user=False - 快速启用、内部统一 IT 管理:可以考虑
create_user=True
5. 验证 TLS 与 bind 方式
- 匿名 bind 是否被允许?
- binddn 是否只具备查询权限?
- STARTTLS 是否真的成功?
6. 设计故障时的回退策略
比如:
- LDAP 服务短时不可用时,管理员如何进入系统?
- 是否保留少量应急本地管理员?
- 应急账号的命名是否避免和 LDAP 员工同名?
这一步不做,真正故障时会非常被动。
这套设计最适合什么团队?
我认为 Odoo 标准 auth_ldap 特别适合这类组织:
- 已有稳定 AD / OpenLDAP;
- 希望统一员工身份入口;
- 但仍接受 Odoo 本地保留用户、权限、公司等业务语义;
- 不要求“LDAP 组自动精细映射 Odoo 角色”一步到位。
换句话说,它是一个身份接入模块,不是完整的 IAM 平台。
如果你把它当成“统一身份 + 自动授权 + 自动生命周期治理”的全能方案,通常会失望;但如果你把它当成“把外部目录身份稳定接进 Odoo 的第一层”,它其实设计得相当克制而靠谱。
最后一句
LDAP 接入最危险的误区,是把“能登录”误认为“设计完成”。
真正决定系统长期稳定的,往往不是那次登录成功,而是源码里这些看起来不起眼的边界:
- 同名本地账号优先谁;
- filter 是否唯一命中;
- 自动开户是否开启;
- Template User 复制了什么;
- 改密后本地密码是否彻底失效。
把这些想清楚,LDAP 才是在给 Odoo 降低管理成本;否则,它只是在把复杂性换个地方藏起来。
DISCUSSION
评论区