框架深潜

Odoo LDAP 登录为什么常常不是“配个地址就完事”:Template User、自动建档与本地密码回退边界讲透

很多团队以为 Odoo 接 LDAP 只是把域控地址填进去就能统一登录。真正决定上线是否稳定的,是过滤器是否只命中一个用户、Template User 复制了哪些权限、何时自动建本地账号,以及 LDAP 改密后为什么要把 Odoo 本地密码清空。

框架
进阶 开发者 3 分钟阅读
0 评论 0 点赞 0 收藏 5 阅读

先说结论

Odoo 的 LDAP 集成并不是“把账号密码丢给目录服务器验证一下”这么简单。

/home/ubuntu/odoo-temp/addons/auth_ldap/models/res_users.pyres_company_ldap.py 看,真正影响成败的有四个关键点:

  1. Odoo 先尝试本地登录,再尝试 LDAP。
  2. LDAP 过滤器必须最终只命中 1 条目录记录。
  3. 本地不存在用户时,是否自动创建、复制谁的权限,决定了首登后的权限轮廓。
  4. 如果密码是在 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 联调里真正要验证的是:

  1. 端口是否通;
  2. binddn 是否可用;
  3. filter 是否唯一命中;
  4. STARTTLS 能否真实握手成功。

缺任何一个,最后的“登录失败”在用户视角里都会长得一模一样。

自动建用户时,Template User 决定的是“复制哪种组织角色”

_get_or_create_user() 是这个模块最有价值的地方。

如果本地没有这个 login,而且 create_user=True,Odoo 会:

  1. 根据 LDAP 条目映射出一组基础值;
  2. 至少写入 namelogincompany_id
  3. 如果 login 看起来像单邮箱,还会顺手写 email
  4. 如果配置了 user(也就是 Template User),就调用 copy(default=values)
  5. 否则直接 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() 的实现是真去做:

  1. 先根据 login 找到唯一 DN;
  2. 用旧密码绑定;
  3. 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

评论区

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