结合公司项目需求,使用PHP编写一个可以批量迁移dn(普通用户账号)的脚本。
长久以来,由于LDAP目录数据库中存放了公司绝大多数在职和已离职员工的基本信息,LDAP的数据库已经变得足够庞大。在LDAP设计之初,LDAP的目录结构和实际的部门组织架构关联地相对紧密,久而久之,由于员工信息变更以及组织架构变更没有和LDAP中的组织架构完全同步,这就导致LDAP数据库中存放的信息和实际相比有了较大的偏差,甚至,某些信息出现了严重的错误,比如:LDAP的目录结构在设计之初,设计者将LDAP中的第二级OU(组织单元)设定成与实际组织架构对应的各个大部门,而每个大部门OU下通常又继续划分了若干个子OU作为该大部门下辖的小部门,有些时候,小部门之间的人员变动相当频繁(跨大部门的变动也时有发生),而LDAP管理员通常无法及时获知这些变动,这就造成有相当一部分的人员在LDAP中划分的部门(OU)与实际所属的部门出现偏差。这些偏差带来的后果是,一旦开发部门开发的WEB系统将LDAP中账户的权限与其所属的父OU关联,那么这些有位置错误的账户就不能准确地获取到相应的权限。
如前所述,尽管“历代”管理员都明白这些问题,但是由于当前使用的LDAP目录中存放的数据十分庞大和繁杂,如果想要彻底清理和归类上万条的记录,其中的风险不言而喻。因此,以往的管理员在获取到部门信息变更的时候,都是进入服务器的后台,使用命令行工具一条一条进行更改,openldap软件在Linux操作系统下提供了ldapadd
、ldapsearch
、ldapdel
等命令用于管理LDAP目录数据。
使用这些命令来迁移某个账号所属OU的思路是,首先用ldapsearch在LDAP的目录数据库中检索到该账号的所有信息,并重定向成ldif格式的文件,ldif文件中存放的就是该账户相关联的全部属性和相应的值,这些属性中,LDAP用于识别该条目在目录结构中位置的标识是dn,例如dn为“uid=person,ou=sectionA,dc=company,dc=net”
的条目,系统会人为这条记录是“ou=sectionA,dc=company,dc=net”
这个ou下的子节点。为了达到重新在LDAP目录的其它位置构造一个属性和原节点完全一致的新节点,可以将已经导出的ldif文件中的dn属性重新修改成父节点为其他ou的新路径,比如,将原dn值改为“uid=person,ou=sectionB,dc=company,dc=net”
,这时候,如果重新将ldif使用ldapadd
(添加条目)命令添加到LDAP的数据库中,系统就会将重新导入的ldif中记录的条目识别成挂载在“ou=sectionB,dc=company,dc=net”
这个ou下的新节点,删除原来的条目后,就能够变相地完成将一条记录从sectionA迁移到sectionB的过程。
使用命令行在后台手动迁移一条记录的过程相当繁琐,而当前的需求是要根据员工实际所属的部门进行大批量的账户迁移,显然,手动迁移是不可能完成的。当然,利用脚本理论上也可以将这些手动的、重复性的操作交给bash shell来自动进行,但是,openldap提供的命令和shell脚本很难进行具体的逻辑控制和迁移结果反馈,大批量操作的情况下十分容易出现一些不可控的情况,此外,LDAP数据库中存放的条目十分繁杂,很多账号要根据不同情况执行不同的迁移操作,这些情况下,shell脚本显得不够安全和可靠,尤其在对安全性要求较高的企业环境下,shell命令和脚本显然不能完成这一艰巨的任务。相对的,PHP语言与生俱来就具有高级语言强大的逻辑控制能力,结合php ldap扩展提供的大量函数,可以便捷地完成区别具体情况大批量操作LDAP数据的任务。
一、思路
显然,直接在公司正在投入使用的LDAP服务器上进行批量的数据更改是极其不现实,考虑到LDAP目录的数据库性质,可以将LDAP服务器中的数据全部导出成ldif格式的文件,再将导出的ldif文件在测试的环境中恢复,同事将LDAP服务器中的配置文件拷贝到测试机中,这样就能在不同的环境中重新构建一个和正式的LDAP服务器一样的测试环境,接下来所有进行迁移和修改dn的操作都在测试环境进行,那么,当所有的调整更新完成以后,就能以同样的方式把测试环境中调整完成的数据库重新导出ldif,把最新的ldif还原到正式的ldap服务器上,如果整个过程不出现意外的话,这样操作可以把这次调整给用户带来的影响降到最低。
抛开运维需要考虑的因素,实现具体操作的代码也很关键。
公司当前的LDAP目录大致是这样的结构:所有的用户账户条目都存放在根节点“dc=company,dc=net”
(假设)下,根节点之下的第一层目录存储的是具体的大部门(类似“研发部”和“销售部”),进入第一级目录后,大多数一级部门(不排除少数情况)下还是会继续划分成若干个二级部门(OU),继续深入二级部门后,除了极少数的情况,大多数二级部门的OU下就存放着“理论上”归属于该部门的所有dn(在职员工账号),根据需求,这次需要核实和迁移的问题账号主要就混杂在这些普通的在职员工账号中。对于离职员工的账号信息,目前并没有将这些账户从公司的LDAP服务器中删除,而是将这些账号统一迁移到了一个名为“Dimission”的OU下,因此这个专门用来存放所有离职人员的OU是所有OU中含有子节点最多的。此外,公司目前对LDAP的使用不仅仅局限在在职员工登陆WEB系统时候调用认证,还有一些公共账户和机器账户也是使用LDAP来进行认证,由于这次操作只是针对在职员工进行的,因此,在编写代码的时候还需要考虑鉴别这些特殊的账号。
由于已经有人事部门提供的准确的人员部门信息,因此考虑逐个部门整理实际属于该部门的账号。在LDAP目录中,每个ou下面实际包含的账号有可能已经是实际属于当前部门的账户,或者是属于其他部门的混乱账号,同时还有可能是已经离职但是没有处理的“僵尸账号”,总而言之,脚本需要兼顾多种可能的情况才能完成最终于人事名单同步的任务。
二、步骤
1、连接LDAP服务器:
$ldap_host="192.168.140.130";
$ldap_port="389";
$ldap_username="cn=manager,dc=company,dc=net";
$ldap_password="password";
//连接LDAP服务器
$conn=ldap_connect($ldap_host,$ldap_port) or die("连接LDAP服务器失败!<br />");
// 设置LDAP版本信息,必须在绑定LDAP之前,否则无效!!!
ldap_set_option($conn, LDAP_OPT_PROTOCOL_VERSION, 3) or die("设置LDAP版本信息失败!<br />");
// 绑定LDAP管理员账户
ldap_bind($conn,$ldap_username,$ldap_password) or die("绑定LDAP管理员账户失败!<br />");
2、定义名单文件和要调整的部门名称
//定义当前调整的部门
$dest_ou_name = "部门A";
//标准名单文件名
$filename = "list";
//定义部门中文名与dn映射关系的数组
$departments = array(
"部门A" => "ou=sectionA,ou=departmentA,dc=company,dc=net",
"部门B" => "ou=sectionB,ou=departmentA,dc=company,dc=net",
"部门C" => "ou=sectionC,ou=departmentB,dc=company,dc=net",
"部门D" => "ou=sectionD,ou=departmentB,dc=company,dc=net",
...//以此类推
);
3、有可能手动输入要调整的部门名称有误(定义的关联数组中没有相应的键)
array_key_exists($dest_ou_name, $departments) or exit("目标部门中文名称索引不存在,请检查!!!<br />");
4、从指定的文件中读取信息,由于公司员工账户中都具有工号属性而且能保证全局唯一,所以使用工号作为操作不同条目的标准,按行读取工号信息到数组后,需要对数组中的每个工号进行合法性检查,定义三个函数,分别检查:列表提供的工号是否存在(即能否在根节点下搜索到)、提供的工号是否已经离职、提供的工号已经在当前要迁移的目标OU中。
//读取list文件中要迁移部门的用户的employeeNumber
$lists=file($filename);
$employees=array();
foreach ($lists as $key => $value) {
$employees[$key]=trim($value);
}
//遍历数组,合法性检查
foreach ($employees as $employeenumber) {
//检查工号是否在LDAP中,否则exit
is_employeenumber_in_ou($conn, $base_dn, $employeenumber) or exit("工号".$employeenumber."不存在,请检查!!!<br />");
//检查工号是否在Dimission中(已经离职),是则exit
is_employeenumber_in_ou($conn, "ou=Dimission,dc=company,dc=net", $employeenumber) and exit("工号".$employeenumber."已经离职,请检查!!!<br />");
//检查工号是否已经在目标ou中,是则exit
is_employeenumber_in_ou($conn, $departments[$dest_ou_name], $employeenumber) and exit("工号".$employeenumber."已经在".$dest_ou_name."中,请检查!!!<br />");
}
5、如果从文件中读到的工号都能顺利通过检查,下一步就能对这些账号进行迁移操作了
//定义三个空数组用于存放发生错误的记录
//存放重复的工号
$duplicate_employeenumber=array();
//存放迁移失败的记录
$failed_migrate_employeenumber=array();
//存放迁移位置成功,但是修改department属性(部门名称字段)失败的记录
$failed_change_employeenumber=array();
//开始遍历数组
foreach ($employees as $employeenumber) {
//一旦工号已经在目标部门
if (is_employeenumber_in_ou($conn, $departments[$dest_ou_name], $employeenumber)) {
$duplicate_employeenumber[]=$employeenumber;
//更新department字段
$employee_dn=get_dn_by_employeenumber($conn, $base_dn, $employeenumber);
change_departmentNumber($conn, $employee_dn, $dest_ou_name) or $failed_change_employeenumber[]=$employeenumber;
//不进行下一步操作,跳过本轮循环
continue;
}
//迁移DN到目标OU部门中
$migrate_result=migrate_dn_by_employeenumber($conn, $base_dn, $departments[$dest_ou_name], $employeenumber);
//如果迁移失败
if (!$migrate_result) {
$failed_migrate_employeenumber[]=$employeenumber;
}
else{
//迁移公共则修改department字段
$employee_dn=get_dn_by_employeenumber($conn, $base_dn, $employeenumber);
//如果修改失败则记录之
change_departmentNumber($conn, $employee_dn, $dest_ou_name) or $failed_change_employeenumber[]=$employeenumber;
}
}
6、打完收工,打印迁移成功信息!
echo "迁移".$filename."中员工到".$dest_ou_name."完成!!!<br />";
echo "############################<br />";
echo "从".$filename."中共读取到的工号条数:".count($employees)."<br />";
echo "############################<br />";
echo "已经在当前部门中的记录条数:".count($duplicate_employeenumber)."<br />";
//如果没有重复的工号
if (count($duplicate_employeenumber)!=0) {
print_r($duplicate_employeenumber);
echo "<br />";
}
echo "############################<br />";
echo "迁移部门失败的记录条数:".count($failed_migrate_employeenumber)."<br />";
//如果有迁移失败的工号
if (count($failed_migrate_employeenumber)!=0) {
print_r($failed_migrate_employeenumber);
echo "<br />";
}
echo "############################<br />";
echo "迁移部门成功但修改部门名称失败的记录条数:".count($failed_change_employeenumber)."<br />";
//如果有迁移位置成功但修改部门名称失败的记录
if (count($failed_change_employeenumber)!=0) {
print_r($failed_change_employeenumber);
echo "<br />";
}
//关闭已经建立的连接
ldap_close($conn);
7、引入自定义函数,通过封装一些原始的LDAP函数,使PHP操作LDAP更加便捷。
/**
* 指定过滤条件搜索条目信息
* @param [type] $conn LDAP连接
* @param [type] $base_dn 搜索的根dn
* @param [type] $filter 过滤条件
* @return [type] 返回关联数组
*/
function search_dn_by_filter($conn, $base_dn, $filter)
{
$result=ldap_search($conn, $base_dn,$filter);
$entries=ldap_get_entries($conn, $result);
for ($i=0; $i < $entries["count"]; $i++) {
$array[$i] = $entries[$i]["dn"];
}
return $array;
}
/**
* 通过工号获取账户的uid
* @param [type] $conn LDAP连接
* @param [type] $base_dn 搜索的根dn
* @param [type] $employeenumber 工号
* @return [type] 获取成功则返回字符串uid,否则返回NULL
*/
function get_uid_by_employeenumber($conn, $base_dn, $employeenumber)
{
$result=ldap_search($conn, $base_dn, "employeenumber=$employeenumber");
$entries=ldap_get_entries($conn, $result);
if (ldap_count_entries($conn, $result)==0) {
return NULL;
}
else{
$uid=$entries[0]["uid"][0];
return $uid;
}
}
/**
* 检查指定的OU中是否存在指定工号的dn
* @param [type] $conn LDAP连接
* @param [type] $ou 要检查的OU
* @param [type] $employeenumber 要检索的工号
* @return boolean 存在则返回true,否则返回false
*/
function is_employeenumber_in_ou($conn, $ou, $employeenumber)
{
$result=ldap_search($conn, $ou, "employeeNumber=$employeenumber");
if (ldap_count_entries($conn, $result)==0) {
return false;
}
else{
return true;
}
}
/**
* 通过工号属性获取相应条没有的dn名称
* @param [type] $conn LDAP连接
* @param [type] $base_dn 搜索的根dn
* @param [type] $employeenumber 工号
* @return [type] 检索到dn后返回字符串,否则返回false
*/
function get_dn_by_employeenumber($conn, $base_dn, $employeenumber)
{
$dn=search_dn_by_filter($conn, $base_dn, "employeenumber=$employeenumber");
if(count($dn)==1){
return $dn[0];
}
else{
return false;
}
}
/**
* 修改dn中的departmentNumber(部门名称)字段
* @param [type] $conn LDAP连接
* @param [type] $dn 要修改的条目的dn
* @param [type] $New_departmentNumber 修改后的部门名称
* @return [type] 返回修改后结果,修改成功范湖true,否则返回false
*/
function change_departmentNumber($conn, $dn, $New_departmentNumber)
{
$result=ldap_modify($conn, $dn, array('departmentnumber' => $New_departmentNumber));
return $result;
}
/**
* 通过工号将条目移动到新位置
* @param [type] $conn LDAP连接
* @param [type] $base_dn 搜索的根dn
* @param [type] $newparent 新的父节点
* @param [type] $employeenumber 要迁移的dn的工号
* @return [type] 迁移成功则返回true,否则返回false
*/
function migrate_dn_by_employeenumber($conn, $base_dn, $newparent, $employeenumber)
{
$dn=get_dn_by_employeenumber($conn, $base_dn, $employeenumber);
$newrdn="uid=".get_uid_by_employeenumber($conn, $base_dn, $employeenumber);
$result=ldap_rename($conn, $dn, $newrdn, $newparent, true);
return $result;
}