news 2026/9/15 16:16:25

mailcow 集成 Adldap2 的 LDAP Contact 模型:创建、成员关系与 Schema 扩展实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
mailcow 集成 Adldap2 的 LDAP Contact 模型:创建、成员关系与 Schema 扩展实践

mailcow 集成 Adldap2 的 LDAP Contact 模型:创建、成员关系与 Schema 扩展实践

【免费下载链接】mailcow-dockerizedmailcow: dockerized - 🐮 + 🐋 = 💕项目地址: https://gitcode.com/GitHub_Trending/ma/mailcow-dockerized

本篇技术指南聚焦 mailcow-dockerized 仓库中随附的 Adldap2 LDAP 库的 Contact(联系人)模型,讲解如何在 Active Directory / OpenLDAP 目录中创建、读写联系人对象,梳理其与 User 模型的区别与继承关系,并结合 mailcow 的 LDAP 同步场景(data/conf/phpfpm/crons/ldap-sync.php)说明实际使用方式。读完本文,你将掌握通过$provider->make()->contact()创建联系人、利用HasMemberOf处理组成员关系,以及通过自定义 Schema 扩展 Contact 模型的完整套路。

Contact 模型的定位:Adldap2 模型体系中的"轻量用户"

在 Adldap2 的模型体系中,Adldap\Models\Contact是所有 LDAP 模型中最特殊的一类:它没有属于自己的专属方法或专属属性,而是全部继承自基类。官方文档(data/web/inc/lib/vendor/adldap2/adldap2/docs/models/contact.md)对此的表述是:

The Contact model extends from the baseAdldap\Models\Modelclass and contains no specific methods / attributes that are limited to it.

从仓库源码看,这一点得到了精确印证。Contact类的完整实现只有短短十几行(data/web/inc/lib/vendor/adldap2/adldap2/src/Models/Contact.php):

namespace Adldap\Models; /** * Class Contact. * * Represents an LDAP contact. */ class Contact extends Entry { use Concerns\HasMemberOf; use Concerns\HasUserProperties; }

三个关键信息:

  1. 继承自Entry而非直接继承ModelEntry位于Adldap\Models命名空间下,是目录中"条目"的通用抽象;而Model是整个模型体系的基类。从源码结构看,Contact通过Entry间接获得了基类Model的全部能力(属性读写、save()create()update()delete()move()rename()、DN 构建等)。
  2. 使用了HasMemberOftrait:联系人可以像用户、组一样参与组成员关系,能够查询、添加、移除所在组(详见下文)。
  3. 使用了HasUserPropertiestrait:联系人虽然不拥有账号属性(密码、登录名等),但可以直接复用"用户资料类"属性(姓名、邮箱、电话、部门、地址等),这也是它被称为"轻量用户"的原因。

创建 Contact 对象

Contact 模型本身不提供构造器级别的差异,创建它的标准方式是通过 provider 的模型工厂make(),并传入初始化属性数组:

// Adldap\Models\Contact $contact = $provider->make()->contact([ 'cn' => 'Suzy Doe', ]);

这里$provider是已连接 LDAP 服务器的 provider 实例(Adldap\Connections\ProviderInterface)。上述调用等价于"构造一个 cn 为 'Suzy Doe' 的 Contact 模型实例",但此时它尚未写入目录——需要后续调用$contact->save()才会真正持久化。

从源码层面看,工厂方法contact()(data/web/inc/lib/vendor/adldap2/adldap2/src/Models/Factory.php)实际做了三件事:

public function contact(array $attributes = []) { $model = $this->schema->contactModel(); return (new $model($attributes, $this->query)) ->setAttribute($this->schema->objectClass(), [ $this->schema->top(), $this->schema->person(), $this->schema->organizationalPerson(), $this->schema->contact(), ]); }
  • 通过$this->schema->contactModel()获取当前 Schema 对应的 Contact 模型类(默认Adldap\Models\Contact,Schema.php);
  • 实例化模型并绑定查询构建器$this->query
  • 自动写入objectClass属性,值为四个对象类的组合:toppersonorganizationalPersoncontact

也就是说,调用make()->contact()后,对象已经被预置了完整的 LDAP 对象类链,无需手动设置objectClass。这是 Contact 与 User 在目录 schema 层面最本质的区别:Contact 使用contact对象类,而 User 使用user对象类,二者都继承自top -> person -> organizationalPerson

完整创建流程示例

结合基类能力(docs/models/model.md),一个完整的"创建并保存联系人"流程如下:

// 1. 通过工厂创建 Contact 并填入初始属性 $contact = $provider->make()->contact([ 'cn' => 'Suzy Doe', ]); // 2. 使用 DN 构建器指定联系人所在的组织单元 // 生成的 DN 形如:CN=Suzy Doe,OU=Contacts,DC=acme,DC=org $dn = $contact->getDnBuilder()->addOu('Contacts'); $contact->setDn($dn); // 3. 补充联系人资料(HasUserProperties 提供) $contact->setFirstName('Suzy'); $contact->setLastName('Doe'); $contact->setEmail('suzy.doe@acme.org'); $contact->setTelephoneNumber('+1 555 0100'); $contact->setDepartment('Public Relations'); $contact->setTitle('PR Manager'); // 4. 持久化到 LDAP 目录 if ($contact->save()) { // 保存成功后,模型属性会从服务器重新同步 echo $contact->exists; // true } else { // 保存失败处理 }

要点说明:

  • save()返回布尔值;保存成功后模型属性会自动与 LDAP 服务器重新同步(见 model.md 的 Note),因此同一请求内可以立刻继续基于该模型做其他操作;
  • 若确定对象尚不存在,可改用create();若确定对象已存在,可改用update()
  • 与 User 模型不同,创建 Contact 不需要设置密码,因此不要求连接启用 SSL/TLS(User 文档中明确提示设置密码时 SSL/TLS 必须开启,见 user.md,Contact 无此约束)。

Contact 可用的属性与方法

由于 Contact 继承自Entry/Model并组合了HasUserPropertiestrait,它可以使用的 API 分为两大类。

1. 全部模型通用的基类方法(来自Model/Entry

以下是文档列出的通用 getter(model.md),Contact 全部可用:

$contact->getName(); // 'name' 属性 $contact->getCommonName(); // 'cn' 属性 $contact->getDisplayName(); // 'displayname' 属性 $contact->getAccountName(); // 'samaccountname'(Contact 上通常为空) $contact->getCreatedAt(); // 'whencreated' 属性 $contact->getCreatedAtDate(); // MySQL 时间戳格式 $contact->getCreatedAtTimestamp();// Unix 时间戳格式 $contact->getUpdatedAt(); // 'whenchanged' 属性 $contact->getObjectClass(); // 'objectclass' 属性 $contact->getObjectCategory(); // 根对象类别字符串 $contact->getObjectSid(); // 二进制 SID $contact->getObjectGuid(); // 二进制 GUID $contact->getConvertedSid(); // 字符串 SID $contact->getConvertedGuid(); // 字符串 GUID $contact->getPrimaryGroupId(); // 主组 ID

通用的属性读写、增删改查能力同样继承自基类:

// 读取 $contact->getAttributes(); // 全部属性数组 $contact->getAttribute('mail'); // 邮箱数组,无则 null $contact->getFirstAttribute('mail'); // 第一个邮箱 $contact->mail; // 属性方式访问 $contact->mail[0]; // 第一个邮箱 // 写入 $contact->setAttribute('cn', 'New Name'); // 方法方式 $contact->setFirstAttribute('mail', 'a@b.c'); // 覆盖第一个值 $contact->cn = 'New Name'; // 属性方式 $contact->fill(['cn' => 'New Name', 'mail' => 'a@b.c']); // 批量填充 // 判断 $contact->hasAttribute('mail'); // 是否含某属性 $contact->countAttributes(); // 属性总数 $contact->inOu('Contacts'); // 是否位于某 OU $contact->isWritable(); // 是否可写 $contact->getDirty(); // 已修改属性 $contact->getOriginal(); // 原始属性

特别提醒:设置布尔型 LDAP 属性时,不能直接使用0/1/true/false(保存时会被转成整数导致 LDAP 服务器报错),必须使用字符串'TRUE'/'FALSE'。这是 Adldap2 在多值属性模型下的一个经典坑,例如:

$contact->setFirstAttribute('showInAddressBook', 'TRUE'); $contact->save();

删除属性时,将属性设为null或调用deleteAttribute();创建新属性时,直接给不存在的属性赋值并在save()时会自动创建,也可以调用createAttribute()/updateAttribute()单独操作。

2. 用户资料类属性(来自HasUserPropertiestrait)

HasUserProperties为 Contact 提供了丰富的"通讯录字段"读写方法(HasUserProperties.php):

方法(getter / setter)对应 LDAP 属性说明
getEmail()/setEmail()mail主邮箱;set 会清空其余邮箱
getFirstName()/setFirstName()givenName
getLastName()/setLastName()sn
getTitle()/setTitle()title职位
getDepartment()/setDepartment()department部门
getCountry()/setCountry()c国家
getStreetAddress()/setStreetAddress()streetAddress街道地址
getPostalCode()/setPostalCode()postalCode邮政编码
getPostOfficeBox()/setPostOfficeBox()postOfficeBox邮箱号(PO Box)
getTelephoneNumber()/setTelephoneNumber()telephoneNumber电话
getFacsimileNumber()/setFacsimileNumber()facsimileTelephoneNumber传真
getMobileNumber()/setMobileNumber()mobile主手机号
getOtherMobileNumber()/setOtherMobileNumber()otherMobile备用手机号
getInitials()/setInitials()initials姓名缩写
getIpPhone()/setIpPhone()ipPhoneIP 电话
getManager()/setManager()manager上级(存 DN)
getMailNickname()mailNickname邮件昵称
getProxyAddresses()/setProxyAddresses()proxyAddresses代理地址数组
addProxyAddress()proxyAddresses追加一个代理地址
getOtherMailbox()/setOtherMailbox()otherMailbox其他邮箱

这些方法内部都通过$this->schema->xxx()将属性名映射为当前目录类型(Active Directory / OpenLDAP / FreeIPA 等)的真实 LDAP 属性名,例如getEmail()实现为$this->getFirstAttribute($this->schema->email()),而schema->email()在 Active Directory Schema 中返回mail

组与成员关系:HasMemberOf 的实战用法

Contact 通过HasMemberOftrait 获得完整的组成员关系能力(源码见 HasMemberOf.php,配套文档见 docs/models/traits/has-member-of.md)。

查询所在组

// 获取联系人所在的所有组(返回 Adldap\Query\Collection,元素为 Group 模型) $groups = $contact->getGroups(); foreach ($groups as $group) { echo $group->getCommonName(); // 如 'PR-Team' } // 只取需要的字段以加速查询 $groups = $contact->getGroups(['cn']); // 递归获取嵌套组 $groups = $contact->getGroups([], true); // 只要组名(一维数组) $names = $contact->getGroupNames(); $names = $contact->getGroupNames(true); // 递归

注意:getGroups()默认只返回直接所属组;要包含嵌套组需传第二个参数true。递归实现中通过$visited数组记录已访问 DN 来防止环形组依赖造成死循环(见 HasMemberOf.php)。

判断成员资格

// 传入 Group 模型 $group = $provider->search()->groups()->find('PR-Team'); if ($contact->inGroup($group)) { /* ... */ } // 传入多个组(数组 / Collection) $groups = $provider->search()->findManyBy('cn', ['PR-Team', 'Office']); if ($contact->inGroup($groups->toArray())) { /* ... */ } // 传入 DN 数组 $dns = ['cn=PR-Team,ou=Groups,dc=acme,dc=org']; if ($contact->inGroup($dns, true)) { /* ... */ } // 传入组名数组 if ($contact->inGroup(['PR-Team', 'Office'], true)) { /* ... */ }

inGroup()的第三参数$recursive控制是否包含嵌套组。从源码看,其判定逻辑支持三种输入形式:Group模型实例(比较 DN)、可解析的 DN 字符串(比较 DN)、普通字符串(比较组cn),见 groupIsParent()。

添加 / 移除组

// 添加:传 Group 模型或组的 DN 字符串均可 $group = $provider->search()->groups()->find('PR-Team'); $contact->addGroup($group); $contact->addGroup('cn=PR-Team,ou=Groups,dc=acme,dc=org'); // 移除 $contact->removeGroup($group); $contact->removeGroup('cn=PR-Team,ou=Groups,dc=acme,dc=org'); // 移除所有组,返回成功移除的组 DN 数组 $removed = $contact->removeAllGroups();

重要:addGroup()/removeGroup()内部直接操作目录,不需要再调用save(),方法返回布尔值,可直接用于if判断。

Contact 与 User 的区别及查询过滤

HasUserProperties让 Contact 拥有和 User 相似的外表,但两者在目录中本质不同:

维度ContactUser
对象类top + person + organizationalPerson + contacttop + person + organizationalPerson + user
账号能力无登录、无密码、无 UAC有登录名、密码、UAC 控制
创建约束无需 SSL/TLS设置密码时要求 SSL/TLS
主组有主组(getPrimaryGroup()
常用场景通讯录、外部联系人、资源对象真实用户账号

Adldap2 的查询构建器也针对这一区别做了处理。在Query\Factory::users()([data/web/inc/lib/vendor/adldap2/adldap2/src/Query/Factory.php#L143-L158])中,查询用户时会额外追加一个过滤条件:排除objectClass=contact的对象,而且该条件仅在 Active Directory Schema 下才加入(OpenLDAP 不支持对 contact 对象类的"不等于"过滤写法):

// OpenLDAP doesn't like specifying the omission of user objectclasses // equal to `contact`. We'll make sure we're working with // ActiveDirectory before adding this filter. if (is_a($this->schema, ActiveDirectory::class)) { $wheres[] = [$this->schema->objectClass(), Operator::$doesNotEqual, $this->schema->objectClassContact()]; }

相应地,查询构建器也提供了专门的contacts()方法(Query/Factory.php),返回限定在 contact 对象类范围内的查询构建器,可用于批量检索联系人。

结合 Schema 扩展 Contact:自定义模型与自定义目录类型

Adldap2 从 v8.0.0 起支持通过自定义 Schema 替换默认模型。如果你的业务需要给 Contact 增加自定义方法,可以按以下步骤(流程见 model.md):

第一步:创建继承自Adldap\Models\Contact的自定义模型

namespace App\Ldap\Models; use Adldap\Models\Contact as Model; class Contact extends Model { public function getFullName() { return trim($this->getFirstName() . ' ' . $this->getLastName()); } }

约束:自定义模型必须继承自现有 Adldap2 模型,因为很多方法与属性只存在于这些类上。

第二步:创建自定义 Schema 并返回你的模型类名

namespace App\Ldap\Schemas; use App\Ldap\Models\Contact; class LdapSchema extends ActiveDirectory { public function contactModel() { return Contact::class; } }

第三步:在连接配置中指定 Schema

$config = [ 'hosts' => ['ldap.corp.local'], 'username' => 'admin', 'password' => 'P@ssword', 'schema' => App\Ldap\Schemas\LdapSchema::class, ]; $ad = new Adldap($config); $provider = $ad->connect(); // 此后所有 contact 结果都会返回你的自定义模型 $contact = $provider->search()->contacts()->find('Suzy Doe');

Schema 是 Adldap2 属性映射的核心:默认ActiveDirectorySchema 定义了所有 LDAP 属性名(contactcontactModelemailmemberOf等,见 Schema.php),仓库同时提供了OpenLDAPFreeIPADirectory389EDirectory等实现(Schemas 目录)。如果你连接的是 OpenLDAP / FreeIPA 等非 Active Directory 服务器,必须在配置中显式切换 schema,否则查询结果可能无法映射到正确的模型实例(setup.md)。

在 mailcow 项目中的实际用途

mailcow-dockerized 将 Adldap2 以 vendor 依赖的形式内置于 Web 前端代码中(位于data/web/inc/lib/vendor/adldap2/adldap2),其 LDAP 相关功能集中在 cron 脚本 data/conf/phpfpm/crons/ldap-sync.php 与 admin 配置页(data/web/templates/admin/tab-ldap.twig)。在实际部署中,Contact 模型主要用于两类场景:

  1. 目录联系人同步:将 AD/LDAP 目录中的 Contact 条目作为通讯录来源同步到邮件系统,供用户在 Webmail / SOGo 中检索外部联系人;
  2. 成员资格驱动的权限派生:利用HasMemberOf的能力,把联系人所在的 AD 组映射为邮件系统的别名/转发规则,实现"按组管理收件人"。

需要强调的是:mailcow 的 LDAP 同步主流程以User 模型为核心(账号登录、邮箱归属),Contact 模型主要用于补充通讯录数据;具体同步哪些对象类、映射哪些属性,取决于你在管理界面 LDAP 配置中的设置,请以实际配置为准。

小结

Adldap2 的 Contact 模型是一个"零专属逻辑"但能力完整的 LDAP 模型:

  • 通过$provider->make()->contact([...])创建,工厂方法自动写入top/person/organizationalPerson/contact对象类;
  • 组合HasMemberOfHasUserProperties,同时获得组成员管理与通讯录字段读写能力;
  • 全部属性读写、增删改查、DN 操作继承自基类Model/Entry
  • 在 Active Directory 下查询用户时会被显式排除,需用contacts()方法单独检索;
  • 需要扩展时,可通过自定义 Schema + 自定义模型无缝替换默认Contact实现。

掌握这些要点后,无论是为 mailcow 补充 LDAP 通讯录同步,还是在其他 PHP 项目中对接 AD/OpenLDAP 联系人数据,都能直接复用上述模式。

延伸阅读

  • Contact 模型官方文档
  • 模型基类:创建 / 更新 / 属性操作 / 移动 / 删除
  • User 模型(含密码创建约束)
  • HasMemberOf Trait 文档
  • Contact 源码实现
  • 模型工厂 contact() 方法
  • Schema 属性映射定义
  • 连接配置与 Schema 选择
  • mailcow LDAP 同步 cron 脚本

【免费下载链接】mailcow-dockerizedmailcow: dockerized - 🐮 + 🐋 = 💕项目地址: https://gitcode.com/GitHub_Trending/ma/mailcow-dockerized

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/15 16:16:06

CTF SQL注入进阶:从171到213题绕过技巧与实战解析

1. 先说清楚:这道题区间到底考什么ctfshow的SQL注入题目从171到213,这一长串数字看起来是个大工程,但你要是真的顺着刷下来,会发现它其实是一条非常清晰的进阶路线。前面几道题还在考最基础的联合查询、报错注入,中间开…

作者头像 李华
网站建设 2026/9/15 16:11:57

基于Matlab与HSV颜色分流的交通标志识别CNN系统设计

简介:这是一份基于Matlab实现的交通标志识别系统毕业设计参考资料,面向计算机、电子信息工程、数学等专业学生在课程设计、期末大作业或毕业设计中的算法仿真与界面开发需求。资源共36个文件,包含mat数据文件、m脚本、jpg/png/bmp图片样本、f…

作者头像 李华
网站建设 2026/9/15 16:11:52

阻抗控制原理与参数整定:让机械臂学会温柔接触

这个系列写到第五期,终于轮到阻抗控制这个绕不开的话题。如果你做机械臂力控、协作机器人二次开发,或者接触过打磨、装配这类需要“温柔接触”的工位,那这个名字你一定不陌生。前几篇我更多聊运动规划和控制结构,这次直接聊“力”…

作者头像 李华
网站建设 2026/9/15 16:11:41

灰色关联分析MATLAB实现:多序列动态相似性量化

简介:本资源是一套面向数据分析初学者与科研人员的灰色关联分析实践工具包,聚焦于在信息不完全场景下量化变量间关联强度的核心需求,适用于工程评估、经济建模、医学指标筛选等实际问题。压缩包共3个文件(2个Excel数据样本、1个Ma…

作者头像 李华
网站建设 2026/9/15 16:11:24

做销售网站要多少钱?这份速查手册帮你看清备案与预算

做销售网站要多少钱?这份速查手册帮你看清备案与预算 备案流程一头雾水,导致项目延期,这是很多刚接手企业建站需求的设计师或前端转运营伙伴最容易踩的坑。你心里其实没底,不知道客户问“做销售网站要多少钱”时,除了报价单上的开发费,后面还藏着多少看不见的隐性成本,尤其是那个让人头疼的ICP备案。别慌,这份【…

作者头像 李华