框架深潜

Odoo LDAP 登录真正难的不是连通,而是 Search/Bind 之后用户如何被映射:登录名、公司与自动建档边界讲透

很多团队把 LDAP 联调理解成‘能连上目录服务器就差不多了’。但 auth_ldap 真正决定上线稳定性的,是搜索过滤器怎么找到唯一 DN、bind 之后如何把目录身份落成 Odoo 用户、company_id 如何选定,以及首次建档到底复制了哪些本地语义。

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

先说结论

很多 LDAP 项目并不是死在“连不上目录服务器”,而是死在 身份映射不稳定

/home/ubuntu/odoo-temp/addons/auth_ldap/models/res_company_ldap.py 可以看出,Odoo 的标准 LDAP 链路其实分成四步:

  1. ldap_filter 搜索目录,必须找到 唯一一条 记录;
  2. 拿到那条记录的 DN,再用用户输入密码做 bind;
  3. 认证成功后,把目录身份映射成 Odoo 本地用户;
  4. 如果本地没有用户,就按公司配置和模板语义创建账号。

所以 LDAP 在 Odoo 里不是“外部验密插件”,而是 把目录身份翻译为本地用户对象 的桥。

_get_entry() 真正做的是“身份决议”

很多人一看到 LDAP 配置,会把注意力放在:

  • 服务器地址
  • 端口
  • binddn
  • 密码

这些当然重要,但源码里 _get_entry() 才是最容易决定成败的函数。

它会:

  • 读取 ldap_filter
  • 统计其中 %s 的占位符数量
  • 用用户输入的 login 去替换这些占位符
  • _query() 去 LDAP 搜索
  • 丢掉没有 DN 的结果
  • 只接受 len(results) == 1

也就是说,0 条不行,多条也不行

这意味着 LDAP 登录不是模糊匹配,而是一次严格的人员解析。你的 filter 不是“搜到一个差不多的人”,而是“把这个登录名唯一落到某个 DN 上”。

为什么这一步比大家想象中更关键

因为后面密码 bind、用户创建、权限复制,全部都建立在“你到底是谁”已经被唯一确定这个前提上。

如果搜索阶段就不稳定:

  • 同一个邮箱命中两条历史账号;
  • uidmail 混用导致多条结果;
  • 离职账号没清理;
  • 大小写和空格不统一;

那后面的失败在用户眼里只会显示成“LDAP 登录不稳定”,但根因其实是 身份决议不唯一

Odoo 把 search 和 bind 明确拆成两段

_query()_authenticate() 的分工很清楚。

_query() 干什么

它使用配置里的:

  • ldap_binddn
  • ldap_password
  • ldap_base
  • ldap_filter

先去目录里找人。

这一段的 bind 身份,通常是:

  • 一个有搜索权限的服务账号;
  • 或者匿名 bind;
  • 或某些目录允许的轻权限查询身份。

_authenticate() 干什么

_get_entry() 拿到唯一 DN 后,Odoo 再重新连接一次,执行:

  • conn.simple_bind_s(dn, password)

这一步才是在验证“用户输入的口令是否真能绑定这个 DN”。

这说明标准 auth_ldap 的心智模型不是“直接拿 login 和 password 去撞 LDAP”,而是:

先解析出用户对象,再验证这个对象的密码。

这个设计非常合理,因为很多目录体系里,最终可 bind 的标识并不是用户登录页里直接输入的那串字符串。

_map_ldap_attributes() 很克制,默认只映射少数字段

很多团队第一次接 LDAP 时,会下意识以为 Odoo 会把目录里的:

  • 部门
  • 岗位
  • 主管
  • 邮箱
  • 手机
  • 组织树

全部自动映射进来。

标准模块并没有这么做。

源码里 _map_ldap_attributes() 默认只构造了这几个值:

  • name: 取 ldap_entry[1]['cn'][0]
  • login: 取当前登录名
  • company_id: 取当前 LDAP 配置所属公司
  • 如果 login 看起来像邮箱,再把 email = login

这很重要。

这说明什么

说明 Odoo 标准模块把 LDAP 视为:

  • 认证来源
  • 以及 最小身份初始化来源

而不是完整的人事主数据同步器。

因此很多实施中的误解,根子就在这里:

  • 以为接完 LDAP,部门组织自动就会对;
  • 以为 LDAP 组会自然变成 Odoo 权限;
  • 以为目录里变更了姓名/邮箱,Odoo 一定同步刷新;

标准模块并不负责这一层。

company_id 来自 LDAP 配置,而不是目录条目动态推断

这也是一个特别容易被忽略的边界。

_map_ldap_attributes() 里,company_id 直接取的是:

  • conf['company'][0]

也就是当前这条 LDAP 配置所属的 Odoo 公司。

这意味着标准模块默认并不会根据 LDAP 条目中的 oudepartmentNumbercompany 等属性动态判断用户该归属哪个公司。

这背后的设计含义非常直接

Odoo 的标准策略是:

先由管理员决定“这条 LDAP 配置服务哪个 Odoo 公司”,再把认证成功的用户放进这个公司语义里。

所以如果你有:

  • 多公司数据库;
  • 多个法人共用同一套 AD;
  • 不同子公司用不同登录入口;

真正稳定的方案往往不是一条 LDAP 配置打天下,而是按公司边界拆配置、拆 filter、拆模板用户。

否则你会遇到很典型的问题:

  • 人是同一个目录里的,但首登落错公司;
  • 复制出来的模板权限属于另一家公司;
  • 邮件、默认公司、可见公司都开始错位。

_get_or_create_user() 是本地身份落地的真正入口

LDAP 认证成功后,Odoo 接下来做的不是“直接给一个临时 session”,而是一定要回到本地 res_users

_get_or_create_user() 的逻辑很值得仔细看:

  1. 先把 login lower().strip()
  2. 直接查 SQL:SELECT id, active FROM res_users WHERE lower(login)=%s
  3. 如果有已激活用户,直接返回该用户 ID;
  4. 如果没有,并且 create_user=True,才创建本地用户;
  5. 若既不存在又不允许自动建档,直接 AccessDenied

这里藏着三个非常关键的边界。

边界一:login 会被标准化

也就是:

  • 大小写不敏感;
  • 首尾空格会被去掉。

所以实施时最稳妥的做法,是从一开始就统一:

  • 登录名到底用邮箱还是员工号;
  • 是否全部小写;
  • 是否允许历史别名继续存在。

边界二:LDAP 最终还是要落到本地用户

Odoo 不是无状态地“验完密码就放行”。

它必须知道:

  • 这个人对应哪个 res.users
  • 属于哪个公司;
  • 拥有哪些组和本地语义。

这就是为什么 LDAP 项目上线后,用户治理仍然是 Odoo 内部治理的一部分。

边界三:create_user 决定的是“认证”和“开户”是否绑定

如果不开自动建档,那么 LDAP 只是证明:

  • 这个目录身份是真的。

但它不自动代表:

  • 这个人有资格进入当前数据库。

这条边界在正式环境里其实非常有价值。

Template User 解决的是“首登角色骨架”,不是目录权限镜像

conf['user'] 有值时,标准模块不是简单 create(values),而是:

  • browse(template_user).copy(default=values)

这说明 Template User 的作用是:

  • 提供一份本地用户骨架;
  • 把组、默认值、偏好等本地语义复制过来;
  • 再把 name/login/company_id 之类覆盖掉。

这和“LDAP 组同步”完全不是一回事。

所以最常见的问题不是 LDAP 验证失败,而是:

  • Template User 太高权,首批用户一上来权限过大;
  • Template User 太偏某部门,别的部门登录后体验完全不对;
  • 多公司环境里模板本身就带着错误公司语义。

为什么标准模块刻意不做“全自动用户自动映射”

如果你从产品设计角度看,会发现 Odoo 这套实现非常保守。

它没有试图:

  • 自动同步整个组织架构;
  • 自动根据 LDAP group 映射用户组;
  • 自动根据目录属性切换多公司可见性;
  • 自动覆盖所有本地字段。

这不是能力不够,而是标准模块在守边界。

因为一旦把这件事做成“目录属性全自动驱动 Odoo 用户对象”,很快就会碰到:

  • 多目录源冲突;
  • 权限放大;
  • 本地手工修正被反复覆盖;
  • 账号治理责任归属不清。

标准模块宁可只提供一条稳健的最小链路:

搜索唯一身份 → bind 验证 → 找本地用户 → 没有则按模板创建。

实战里最该优先检查什么

如果用户反馈“LDAP 有时能进有时不能进”或“新用户首登落错了”,我会按这个顺序排:

  1. 先看 ldap_filter 是否稳定命中唯一 DN
  2. 确认 bind 账号是否真的有搜索权限
  3. 确认 company_id 是否来自正确的 LDAP 配置
  4. 检查 login 规范是否统一且会 lower/strip 后不冲突
  5. 检查 Template User 是否把本地角色骨架带偏
  6. 最后再看是否需要定制属性同步,而不是误以为标准模块已包含这层能力。

一句话记忆

Odoo 的 LDAP 成败,关键不在“目录能不能连上”,而在“Search/Bind 之后,这个人如何被唯一、稳定、可治理地映射成一个本地 Odoo 用户”。

DISCUSSION

评论区

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