openldap 介绍和使用

2020/05/12 ldap,openldap

早期,公司是没有统一认证这个东西的,所以各自玩各自的。于是, confluence 一个用户体系,gitlab 一个用户体系,Jenkins 一个用户体系等等, 开发中要用到的开源软件数不胜数,每个软件都要认证, 必须想办法统一账号。

第三系统的认证通常都是配置化的, 比如 oauth, openid, ldap。兼容最广泛就是 ldap了,虽然是很老的系统(LDAPv3 was developed in the late 1990’s to replace LDAPv2.),最后还是要使用它。

环境说明

  • 宿主机:CentOS 7.7.1908 x86_64
  • 默认系统操作用户:root
  • phpldapadmin 地址:http://HostIP:8080

一、 LDAP 简介

在开始使用之前再明确一下我们的目标。为了统一公司内部的账号登录体系。公司的人员组织架构假设是这样子的:

.
└── company
    ├── ceo-Ryan
    ├── HR
    │   └── hr-a
    ├── 市场
    │   └── a
    ├── 研发
    │   ├── cto-Ryan
    │   ├── dev1
    │   │   ├── dev-Ryan
    │   │   ├── dev-Ryan2
    │   │   └── manager-Ryan
    │   ├── dev2
    │   └── dev3
    └── 行政

实际上会复杂的多,简化的来看分为部门和成员。ldap就是存储这样的数据结构 tree.

咬牙看了很多博客,依旧云里雾里的感觉,最终决定上手尝试,并完整记录整个过程来加深理解。接下来将以一个什么都不懂的角色开始探索和使用 ldap。

以下概念主要面向理解,去除了不关注的官方完整介绍,旨在能够快速了解我们要用的东西是什么样,如果需要更完整的概念介绍,参考后面的参考文献即可。

什么是 LDAP

LDAP 是轻量目录访问协议,英文全称是Lightweight Directory Access Protocol,一般都简称为 LDAP。按照我们对文件目录的理解,LDAP 可以看成一个文件系统,类似目录和文件树。

LDAP 的软件

LDAP 并不是一款软件,而是一个协议。

现在市场上有关 LDAP 的产品已有很多,各大软件公司都在他们的产品中集成了 LDAP 服务,如:

  • Microsoft 的 ActiveDirectory
  • Lotus 的 Domino Directory
  • IBM 的 WebSphere

LDAP 的开源实现是 OpenLDAP,它比商业产品一点也不差,而且源码开放。

OpenLDAP 是最常用的目录服务之一,它是一个由开源社区及志愿者开发和管理的一个开源项目,提供了目录服务的所有功能,包括:目录搜索、身份认证、安全通道、过滤器等等。大多数的 Linux 发行版里面都带有 OpenLDAP 的安装包。

OpenLDAP 服务默认使用非加密的 TCP/IP 协议来接收服务的请求,并将查询结果传回到客户端。由于大多数目录服务都是用于系统的安全认证部分比如:用户登录和身份验证,所以它也支持使用基于 SSL/TLS 的加密协议来保证数据传送的保密性和完整性。

OpenLDAP 是使用 OpenSSL 来实现 SSL/TLS 加密通信的。

ldap 的信息模型

【重要部分】

LDAP 的信息模型是建立在”条目”(entries)的基础上。一个条目是一些属性的集合,并且具有一个全局唯一的”可区分名称”DN,一个条目可以通过DN来引用。每一个条目的属性具有一个类型和一个或者多个值。

类型通常是容易记忆的名称,比如:”cn”是通用名称( common name ) ,或者”mail”是电子邮件地址。

条目的值的语法取决于属性类型。比如:

  • cn 属性可能具有一个值 “Babs Jensen” 。
  • 一个mail属性可能包含 “bbs@kevin.com” 。
  • 一个 jpegphoto 属性可能包含一幅 JPEG(二进制)格式的图片。

LDAP 的 objectClass

LDAP 通过属性 objectClass 来控制哪一个属性必须出现或允许出现在一个条目中,它的值决定了该条目必须遵守的模式 规则(可以理解为关系数据库的表结构)。接下来会用到的 objectClass 有

objectClass 含义
olcGlobal 全局配置文件类型, 主要是 cn=config.ldif 的配置项
top 顶层的对象
organization 组织,比如公司名称,顶层的对象
organizationalUnit 重要, 一个目录节点,通常是 group,或者部门这样的含义
inetOrgPerson 重要, 我们真正的用户节点类型,person 类型, 叶子节点
groupOfNames 重要, 分组的 group 类型,标记一个 group 节点
olcModuleList 配置模块的对象

LDAP 常用关键字列表

关键字 英文全称 含义
dc Domain Component 域名的部分,其格式是将完整的域名分成几部分,如域名为 example.com 变成 dc=example,dc=com
uid User Id 用户 ID,如“tom”
ou Organization Unit 组织单位,类似于 Linux 文件系统中的子目录,它是一个容器对象,组织单位可以包含其他各种对象(包括其他组织单元)
cn Common Name 公共名称,如 “Thomas Johansson”
sn Surname 姓,如 “Johansson”
dn Distinguished Name 惟一辨别名,类似于 Linux 文件系统中的绝对路径,每个对象都有一个惟一的名称,如“uid= tom,ou=market,dc=example,dc=com”,在一个目录树中 DN 总是惟一的
rdn Relative dn 相对辨别名,类似于文件系统中的相对路径,它是与目录树结构无关的部分,如“uid=tom”或“cn= Thomas Johansson”
c Country 国家,如“CN”或“US”等。
o                          Organization 组织名,如“Example, Inc.”

我们把:

  • dn 当做用户唯一主键
  • cn 是 common name,应该等同于用户名,因为用户名必须唯一,通常为邮箱前缀,比如: ryan.miao
  • sn 作为姓氏
  • uid 作为用户 id

通常用户 id 也是唯一的。所以在使用 ldap 做认证的时候,大概逻辑如下:

  1. 配置 ldap host, admin, admin pass
  2. 用户登录时传递 username
  3. 读取配置的 ldap 信息,查询 cn 或者 uid 等于 username 的数据
  4. 取出第一个记录, 获得 dn, 根据 dn 和 password 再次去 ldap 服务器认证。即我们必须保证 cn 或 uid 是全局唯一的,
  5. 认证通常需要进行两次。原因就在于 dn 没办法根据用户名计算出来。

一个 ldap 用户组织可能是这样的:

一个倒桩树组成结构。

二、实战篇

基于 Docker 容器镜像。

1. 安装 Docker、Docker-Compose

# 生成一键安装脚本
$ tee 1key_install_docker.sh <<EOOF
#!/bin/bash

#生成工作目录
mkdir -p /server/{tools,scripts,backup,docker-compose}

##更换阿里云源(针对国内用户,可选!)
mv /etc/yum.repos.d/CentOS-Base.repo{,.backup}
curl -o /etc/yum.repos.d/CentOS-Base.repo http://mirrors.aliyun.com/repo/Centos-7.repo
yum update -y

#安装 docker
curl -fsSL https://get.docker.com | bash -s docker --mirror Aliyun

##镜像加速
mkdir -p /etc/docker
tee /etc/docker/daemon.json <<EOF
{
    "registry-mirrors": [
        "https://1nj0zren.mirror.aliyuncs.com",
        "https://docker.mirrors.ustc.edu.cn",
        "https://registry.docker-cn.com"
    ]
}
EOF

systemctl daemon-reload
systemctl restart docker
systemctl enable docker

## check
docker version

##安装最新 docker-compose
curl -L https://mirrors.aliyun.com/docker-toolbox/linux/compose/$(curl -s https://mirrors.aliyun.com/docker-toolbox/linux/compose/ |egrep '^<a' |awk -F '">|</a>' '{print $2}' |sort -V |tail -1)docker-compose-Linux-x86_64 -o  /usr/bin/docker-compose

chmod +x /usr/bin/docker-compose
docker-compose -v
EOOF

# 进行语法检查
$ sh -n  1key_install_docker.sh

# 执行安装
$ sh 1key_install_docker.sh

相关链接:https://developer.aliyun.com/mirror/docker-ce?spm=a2c6h.13651102.0.0.3e221b11yCPzpk

2. 构建 OpenLDAP 服务

$ mkdir -p /server/docker-compose/openldap
$ cd /server/docker-compose/openldap

# 生成服务编排文件
$ tee docker-compose.yml <<EOF
version: '3'

services:

  openldap:
    image: osixia/openldap:1.3.0
    restart: always
    container_name: ldap
    ports:
      - 389:389
      - 636:636
    volumes:
      - /etc/localtime:/etc/localtime:ro

  phpldapadmin:
    depends_on:
      - openldap
    image: osixia/phpldapadmin:latest
    container_name: phpldapadmin
    environment:
      PHPLDAPADMIN_LDAP_HOSTS: "openldap"
      PHPLDAPADMIN_HTTPS: "false"
    ports:
      - "8080:80"
EOF

# 语法检查
$ docker-compose config

# 启动服务
$ docker-compose up -d

# 查看实时日志
$ docker-compose logs -f

3. 登入 openLDAP 容器,并执行相关 LDAP 操作

# 查看容器
$ docker ps
CONTAINER ID        IMAGE                   COMMAND                 CREATED             STATUS              PORTS                                        NAMES
e53c92de7493        osixia/openldap:1.3.0   "/container/tool/run"   3 days ago          Up 23 minutes       0.0.0.0:389->389/tcp, 0.0.0.0:636->636/tcp   ldap

# 登录容器
$ docker exec -it ldap bash
root@e53c92de7493:/#

# 为方便测试,可更换容器镜像源,并安装常用工具(仅适用国内用户)
root@e53c92de7493:/etc/ldap# tee /etc/apt/sources.list <<EOF
deb http://mirrors.163.com/debian/ buster main non-free contrib
deb http://mirrors.163.com/debian/ buster-updates main non-free contrib
deb http://mirrors.163.com/debian/ buster-backports main non-free contrib
deb http://mirrors.163.com/debian-security/ buster/updates main non-free contrib

deb-src http://mirrors.163.com/debian/ buster main non-free contrib
deb-src http://mirrors.163.com/debian/ buster-updates main non-free contrib
deb-src http://mirrors.163.com/debian/ buster-backports main non-free contrib
deb-src http://mirrors.163.com/debian-security/ buster/updates main non-free contrib
EOF

root@e53c92de7493:/etc/ldap# apt-get update -y

root@e53c92de7493:/etc/ldap# apt-get install tree -y

ldap 的配置文件

安装后的配置目录是: /etc/ldap, 内容包括下列这些文件:

/etc/ldap
├── ldap.conf -> /container/service/slapd/assets/ldap.conf
├── pqchecker
│   └── pqparams.dat
├── sasl2
├── schema
│   ├── collective.ldif
│   ├── collective.schema
│   ├── corba.ldif
│   ├── corba.schema
│   ├── core.ldif
│   ├── core.schema
│   ├── cosine.ldif
│   ├── cosine.schema
│   ├── duaconf.ldif
│   ├── duaconf.schema
│   ├── dyngroup.ldif
│   ├── dyngroup.schema
│   ├── inetorgperson.ldif
│   ├── inetorgperson.schema
│   ├── java.ldif
│   ├── java.schema
│   ├── misc.ldif
│   ├── misc.schema
│   ├── nis.ldif
│   ├── nis.schema
│   ├── openldap.ldif
│   ├── openldap.schema
│   ├── pmi.ldif
│   ├── pmi.schema
│   ├── ppolicy.ldif
│   ├── ppolicy.schema
│   └── README
└── slapd.d
    ├── changeroot.sh
    ├── cn=config
    ├── cn=config.ldif
    ├── docker-openldap-was-admin-password-set
    ├── docker-openldap-was-started-with-tls
    ├── rootdn.ldif
    └── tmp.ldif
cn=config.ldif

默认配置文件,位于 /etc/ldap/slapd.d, 文件格式为 LDAP Input Format (LDIF), ldap 目录特定的格式。

root@e53c92de7493:/etc/ldap/slapd.d# cat cn\=config.ldif
# AUTO-GENERATED FILE - DO NOT EDIT!! Use ldapmodify.
# CRC32 15165520
dn: cn=config
objectClass: olcGlobal
cn: config
olcArgsFile: /var/run/slapd/slapd.args
olcLogLevel: none
olcPidFile: /var/run/slapd/slapd.pid
olcToolThreads: 1
structuralObjectClass: olcGlobal
entryUUID: d12ef22e-2606-103a-8748-c9a21a061f1c
creatorsName: cn=config
createTimestamp: 20200509060524Z
olcTLSCipherSuite: SECURE256:+SECURE128:-VERS-TLS-ALL:+VERS-TLS1.2:-RSA:-DHE
 -DSS:-CAMELLIA-128-CBC:-CAMELLIA-256-CBC
olcTLSCACertificateFile: /container/service/slapd/assets/certs/ca.crt
olcTLSCertificateFile: /container/service/slapd/assets/certs/ldap.crt
olcTLSCertificateKeyFile: /container/service/slapd/assets/certs/ldap.key
olcTLSDHParamFile: /container/service/slapd/assets/certs/dhparam.pem
olcTLSVerifyClient: demand
entryCSN: 20200509060525.871739Z#000000#000#000000
modifiersName: gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth
modifyTimestamp: 20200509060525Z
olcDatabase={1}mdb.ldif

db 存储格式,有 mdb、bdb 和 hdb 三种,这里是hdb. 可以直接查看文件,也可以查询:

root@e53c92de7493:/etc/ldap/slapd.d# ldapsearch -Y EXTERNAL -H ldapi:/// -b cn=config dn | grep olcDatabase
SASL/EXTERNAL authentication started
SASL username: gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth
SASL SSF: 0
dn: olcDatabase={-1}frontend,cn=config
dn: olcDatabase={0}config,cn=config
dn: olcDatabase={1}mdb,cn=config
dn: olcOverlay={0}memberof,olcDatabase={1}mdb,cn=config
dn: olcOverlay={1}refint,olcDatabase={1}mdb,cn=config
dn: olcOverlay={2}memberof,olcDatabase={1}mdb,cn=config

核心配置文件,位于 /etc/ldap/slapd.d/cn=config, 可以配置域名 (olcSuffix), 管理员账号 (olcRootDN) 等。

root@e53c92de7493:/etc/ldap/slapd.d# cat cn\=config/olcDatabase\=\{1\}mdb.ldif
# AUTO-GENERATED FILE - DO NOT EDIT!! Use ldapmodify.
# CRC32 c057ffa1
dn: olcDatabase={1}mdb
objectClass: olcDatabaseConfig
objectClass: olcMdbConfig
olcDatabase: {1}mdb
olcDbDirectory: /var/lib/ldap
olcLastMod: TRUE
olcDbCheckpoint: 512 30
olcDbMaxSize: 1073741824
structuralObjectClass: olcMdbConfig
entryUUID: d12fb894-2606-103a-8751-c9a21a061f1c
creatorsName: cn=admin,cn=config
createTimestamp: 20200509060524Z
olcAccess: {0}to attrs=userPassword by self write by anonymous auth by group
 .exact="cn=g-admin,ou=Group,dc=demo,dc=com" write by * none
olcAccess: {1}to * by self write by group.exact="cn=g-admin,ou=Group,dc=demo
 ,dc=com" write by * none
olcAccess: {2}to attrs=userPassword,shadowLastChange by self write by dn="cn
 =admin,dc=example,dc=org" write by anonymous auth by * none
olcAccess: {3}to * by self read by dn="cn=admin,dc=example,dc=org" write by 
 * none
olcDbIndex: uid eq
olcDbIndex: mail eq
olcDbIndex: memberOf eq
olcDbIndex: entryCSN eq
olcDbIndex: entryUUID eq
olcDbIndex: objectClass eq
olcRootDN: cn=admin,dc=demo,dc=com
olcSuffix: dc=demo,dc=com
olcRootPW:: e1NTSEF9SmFiek9tbjFaSlc0U2hTcFVUaFNBS3BvZ3ppeCtlWXU=
entryCSN: 20200512082802.368305Z#000000#000#000000
modifiersName: gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth
modifyTimestamp: 20200512082802Z

可以看到很多文件名和字段名都有前缀 “olc” (OpenLDAP Configuration), 理解就好。

创建 olcRootDN 作为管理员账号

看到前面两个配置文件,官方不推荐我们直接修改配置文件,而是通过 ldapmodify 来更新配置。

类似于 update by pk, 这里的 pk 就是 dn 了。

创建 rootdn.ldif

root@e53c92de7493:/etc/ldap/slapd.d# tee rootdn.ldif <<EOF
dn: olcDatabase={1}mdb,cn=config
changetype: modify
replace: olcRootDN
olcRootDN: cn=admin,dc=demo,dc=com
-
replace: olcSuffix
olcSuffix: dc=demo,dc=com
-
replace: olcRootPW
olcRootPW: <pass>
EOF
  • 修改 olcRootDN, 设置为我们的 admin: cn=admin,dc=demo,dc=com
  • 修改 olcSuffix, 设置为我们的域名 dc=demo,dc=com
  • 修改 olcRootPW, 设置我们的 admin 密码, 这个需要加密,所以暂时放一个占位符,等下替换
  • changetype 变更类型, replace 表示替换, add 表示增加。

cn=config 是全局配置,必须包含 objectClass: olcGlobal

然后创建 changeroot.sh

root@e53c92de7493:/etc/ldap/slapd.d# cat changeroot.sh
admin_pass=`slappasswd -s admin`
echo "admin pass is:  ${admin_pass}"
sed "s!<pass>!${admin_pass}!g"   rootdn.ldif > tmp.ldif

echo "备份默认配置"

cp /etc/ldap/slapd.d/cn\=config/olcDatabase\=\{1}mdb.ldif /etc/ldap/slapd.d/cn\=config/olcDatabase\=\{1}mdb.ldif.bak

echo "将要修改的内容:"
cat tmp.ldif

ldapmodify -Q -Y EXTERNAL -H ldapi:/// -f tmp.ldif

echo "修改后的变化"
diff /etc/ldap/slapd.d/cn\=config/olcDatabase\=\{1}mdb.ldif /etc/ldap/slapd.d/cn\=config/olcDatabase\=\{1}mdb.ldif.bak
  • slappasswd -s admin 获取加密后的密码
  • 备份原始文件
  • ldapmodify 更新命令, -H指定host,这里ldapi:/// 表示IPC (Unix-domain socket)协议, -f 指定变更的内容。

命令文档: http://man7.org/linux/man-pages/man1/ldapmodify.1.html

使用脚本进行变更,而不是直接命令行交互式变更,这样可以更容易梳理变更逻辑, 而且可以重复使用。

验证

通过 diff,可以看到配置文件已经发生了变更

root@e53c92de7493:/etc/ldap/slapd.d# sh changeroot.sh
修改后的变化
2c2
< # CRC32 b643556d
---
> # CRC32 9b5dd3fc
7a8,9
> olcSuffix: dc=my-domain,dc=com
> olcRootDN: cn=Manager,dc=my-domain,dc=com
14,19c16,18
< olcRootDN: cn=admin,dc=demo,dc=com
< olcSuffix: dc=demo,dc=com
< olcRootPW:: e1NTSEF9Q3puZEw4QzN4aWJNQTlHeEpYV2doNEN3NHJXSm5Fb0s=
< entryCSN: 20190814074323.492640Z#000000#000#000000
< modifiersName: gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth
< modifyTimestamp: 20190814074323Z
---
> entryCSN: 20190814074118.406339Z#000000#000#000000
> modifiersName: cn=config
> modifyTimestamp: 20190814074118Z

我们可以通过 search 语法来确定账号密码是否正确:

root@e53c92de7493:/etc/ldap/slapd.d# ldapsearch -H ldapi:///  -D "cn=admin,dc=demo,dc=com" -w admia
ldap_bind: Invalid credentials (49)

root@e53c92de7493:/etc/ldap/slapd.d# ldapsearch -H ldapi:///  -D "cn=admin,dc=demo,dc=com" -w admin
# extended LDIF
#
# LDAPv3
# base <> (default) with scope subtree
# filter: (objectclass=*)
# requesting: ALL
#

# search result
search: 2
result: 32 No such object

# numResponses: 1

ldapsearch 查询语法:

  • -H 指定 host
  • -D 指定 admin 的账号,即 rootdn
  • -w 指定密码
  • -x 启用认证

添加我们的 base 组织结构

有了管理员,还需要配置组织结构 base.ldif。在这之前,我们需要导入一些模板。schema 类似数据库表定义,定义了字段名称和类型。

schema地址:/etc/ldap/schema

默认安装加载了 core.ldif , 我们现在加载几个想要的 schema:

ldapadd -Y EXTERNAL -H ldapi:/// -f /etc/ldap/schema/cosine.ldif
ldapadd -Y EXTERNAL -H ldapi:/// -f /etc/ldap/schema/nis.ldif
ldapadd -Y EXTERNAL -H ldapi:/// -f /etc/ldap/schema/inetorgperson.ldif

然后创建文件 base.ldif

# 为方便调试,讲自定义 LDIF 文件放到用户根目录 ~
root@e53c92de7493:/etc/ldap/slapd.d# cd ~

root@e53c92de7493:~# tee base.ldif <<EOF
dn: dc=demo,dc=com
objectClass: top
objectClass: dcObject
objectClass: organization
o: ldap测试组织
dc: demo

dn: cn=Manager,dc=demo,dc=com
objectClass: organizationalRole
cn: Manager
description: 组织管理人

dn: ou=People,dc=demo,dc=com
objectClass: organizationalUnit
ou: People

dn: ou=Group,dc=demo,dc=com
objectClass: organizationalUnit
ou: Group
EOF

使用 ldapadd 命令添加 base:

root@e53c92de7493:~# ldapadd -x -D cn=admin,dc=demo,dc=com -w admin -f base.ldif

使用 ldapsearch 来检查内容

root@e53c92de7493:~# ldapsearch -x -D cn=admin,dc=demo,dc=com -w admin -b "dc=demo,dc=com"
# extended LDIF
#
# LDAPv3
# base <dc=demo,dc=com> with scope subtree
# filter: (objectclass=*)
# requesting: ALL
#

# demo.com
dn: dc=demo,dc=com
objectClass: top
objectClass: dcObject
objectClass: organization
o:: bGRhcOa1i+ivlee7hOe7hw==
dc: demo

# Manager, demo.com
dn: cn=Manager,dc=demo,dc=com
objectClass: organizationalRole
cn: Manager
description:: 57uE57uH566h55CG5Lq6

# People, demo.com
dn: ou=People,dc=demo,dc=com
objectClass: organizationalUnit
ou: People

# Group, demo.com
dn: ou=Group,dc=demo,dc=com
objectClass: organizationalUnit
ou: Group

# search result
search: 2
result: 0 Success

# numResponses: 5
# numEntries: 4

-x 启用认证 -D bind admin 的 dn -w admin的密码 -b basedn, 查询的基础 dn 可以看到中文被替换成 hash, 后面可以通过其他方式看到

添加人员

ou 并不能当做分组,而仅仅是组织架构的一个单元。ldap的分组都是通过单独的 group 来实现的。

添加人员对应的是树的叶子节点,使用的

oebjectClass: inetOrgPerson

添加组织部门对应的是目录,使用的

objectClass: organizationalUnit

我们要把人员添加到 ou=People,dc=demo,dc=com 下。

创建 adduser.ldif

root@e53c92de7493:~# tee adduser.ldif <<EOF
dn: ou=研发部门,ou=People,dc=demo,dc=com
changetype: add
objectClass: organizationalUnit
ou: 研发部门

dn: ou=后台组,ou=研发部门,ou=People,dc=demo,dc=com
changetype: add
objectClass: organizationalUnit
ou: 后台组

dn: cn=ryan.miao,ou=后台组,ou=研发部门,ou=People,dc=demo,dc=com
changetype: add
objectClass: inetOrgPerson
cn: ryan.miao
departmentNumber: 1
sn: Miao
title: 大牛
mail: ryan.miao@demo.com
uid: 10000
displayName: 中文名

dn: cn=someone,ou=后台组,ou=研发部门,ou=People,dc=demo,dc=com
changetype: add
objectClass: inetOrgPerson
cn: someone
departmentNumber: 1
sn: someone
title: Java工程师
mail: someone@demo.com
uid: 10001
displayName: 某人

dn: ou=测试组,ou=研发部门,ou=People,dc=demo,dc=com
changetype: add
objectClass: organizationalUnit
ou: 测试组

dn: cn=tester.miao,ou=测试组,ou=研发部门,ou=People,dc=demo,dc=com
changetype: add
objectClass: inetOrgPerson
cn: tester.miao
departmentNumber: 2
sn: Miao
title: 测试工程师
mail: tester@demo.com
uid: 10002
displayName: 测试某人

dn: ou=HR,ou=People,dc=demo,dc=com
changetype: add
objectClass: organizationalUnit
ou: HR

dn: cn=fang.huang,ou=HR,ou=People,dc=demo,dc=com
changetype: add
objectClass: inetOrgPerson
cn: fang.huang
departmentNumber: 3
sn: Huang
title: HRBP
mail: fang.huang@demo.com
uid: 10003
displayName: 黄芳

使用 ldapadd 来添加我们的用户:

root@e53c92de7493:~# ldapadd -x -D cn=admin,dc=demo,dc=com -w admin -f adduser.ldif
adding new entry "ou=研发部门,ou=People,dc=demo,dc=com"

adding new entry "ou=后台组,ou=研发部门,ou=People,dc=demo,dc=com"

adding new entry "cn=ryan.miao,ou=后台组,ou=研发部门,ou=People,dc=demo,dc=com"

adding new entry "cn=someone,ou=后台组,ou=研发部门,ou=People,dc=demo,dc=com"

adding new entry "ou=测试组,ou=研发部门,ou=People,dc=demo,dc=com"

adding new entry "cn=tester.miao,ou=测试组,ou=研发部门,ou=People,dc=demo,dc=com"

adding new entry "ou=HR,ou=People,dc=demo,dc=com"

adding new entry "cn=fang.huang,ou=HR,ou=People,dc=demo,dc=com"

使用 ldapsearch 来查询用户

指定唯一 id 来查询某个用户,比如 cn 唯一,则

root@e53c92de7493:~# ldapsearch -x -D cn=admin,dc=demo,dc=com -w admin -b "dc=demo,dc=com" "cn=ryan.miao"
# extended LDIF
#
# LDAPv3
# base <dc=demo,dc=com> with scope subtree
# filter: cn=ryan.miao
# requesting: ALL
#

# ryan.miao, \E5\90\8E\E5\8F\B0\E7\BB\84, \E7\A0\94\E5\8F\91\E9\83\A8\E9\97\A8,
  People, demo.com
dn:: Y249cnlhbi5taWFvLG91PeWQjuWPsOe7hCxvdT3noJTlj5Hpg6jpl6gsb3U9UGVvcGxlLGRjP
 WRlbW8sZGM9Y29t
objectClass: inetOrgPerson
cn: ryan,miao
cn: ryan.miao
departmentNumber: 1
sn: Miao
title:: 5aSn54mb
mail: ryan.miao@demo.com
uid: 10000
displayName:: 5Lit5paH5ZCN

# search result
search: 2
result: 0 Success

# numResponses: 2
# numEntries: 1

和前面的示例相比,多了一个参数 filter

ldapsearch -x -D “admin的dn” -w “admin的密码” -b “basedn, 最外层的分组” “search filter:”

还可以指定返回的字段

root@e53c92de7493:~# ldapsearch -x -D cn=admin,dc=demo,dc=com -w admin -b "ou=HR,,dc=demo,dc=com" cn uid displayName
# extended LDIF
#
# LDAPv3
# base <ou=HR,ou=People,dc=demo,dc=com> with scope subtree
# filter: (objectclass=*)
# requesting: cn uid displayName
#

# HR, People, demo.com
dn: ou=HR,ou=People,dc=demo,dc=com

# fang.huang, HR, People, demo.com
dn: cn=fang.huang,ou=HR,ou=People,dc=demo,dc=com
cn: fang.huang
uid: 10003
displayName:: 6buE6Iqz

# search result
search: 2
result: 0 Success

# numResponses: 3
# numEntries: 2

在配置第三方认证的时候,比如 airflow, 就是通过这样 userfilter 来 search 用户的。

添加用户密码

刚才添加用户太快,忘记添加用户密码了。这就涉及到添加用户的同时指定密码和 admin 修改密码以及用户自己修改密码三个情况了。

添加用户的时候指定密码

一个 hr 肯定太累了,添加一个新的 hr hr-ryan

创建文件 addone.ldif

root@e53c92de7493:~# tee addone.ldif <<EOF
dn: cn=hr-ryan,ou=HR,ou=People,dc=demo,dc=com
changetype: add
objectClass: inetOrgPerson
cn: hr-ryan
userPassword: 123456
departmentNumber: 3
sn: hr-ryan
title: HRBP
mail: hr-ryan@demo.com
uid: 10004
displayName: 我是猎头
EOF

执行添加

ldapadd -x -D cn=admin,dc=demo,dc=com -w admin -f addone.ldif

查询验证

root@e53c92de7493:~# ldapsearch -x -D cn=admin,dc=demo,dc=com -w admin -b dc=demo,dc=com
# extended LDIF
#
# LDAPv3
# base <dc=demo,dc=com> with scope subtree
# filter: cn=hr-*
# requesting: ALL
#

# hr-ryan, HR, People, demo.com
dn: cn=hr-ryan,ou=HR,ou=People,dc=demo,dc=com
objectClass: inetOrgPerson
cn: hr-ryan
userPassword:: MTIzNDU2
departmentNumber: 3
sn: hr-ryan
title: HRBP
mail: hr-ryan@demo.com
uid: 10004
displayName:: 5oiR5piv54yO5aS0

# search result
search: 2
result: 0 Success

# numResponses: 2
# numEntries: 1

可以看到,filter 里可以使用通配符。并且,用户密码被加密了。

我们前文说,第三方系统:

  • 第一步: 通过 search 拿到 dn,也就是上面这一步。
  • 第二步:是验证密码

验证密码是怎么做的呢?直接通过 search 语法连接 ldap,通过则证明密码正确。

root@e53c92de7493:~# ldapsearch -x -D cn=hr-ryan,ou=HR,ou=People,dc=demo,dc=com -w 123456
# extended LDIF
#
# LDAPv3
# base <> (default) with scope subtree
# filter: (objectclass=*)
# requesting: ALL
#

# search result
search: 2
result: 32 No such object

# numResponses: 1

修改用户密码

管理员权限最大,可以修改任意密码。使用 ldapmodify

创建文件 updatepass.ldif

root@e53c92de7493:~# tee updatepass.ldif <<EOF
dn: cn=ryan.miao,ou=后台组,ou=研发部门,ou=People,dc=demo,dc=com
changetype: modify
replace: userPassword
userPassword: ryanmiao
EOF

执行修改

root@e53c92de7493:~# ldapmodify -a  -D "cn=admin,dc=demo,dc=com" -w admin -f updatepass.ldif

modifying entry "cn=ryan.miao,ou=后台组,ou=研发部门,ou=People,dc=demo,dc=com"

查询确认

root@e53c92de7493:~# ldapsearch -x -D cn=ryan.miao,ou=后台组,ou=研发部门,ou=People,dc=demo, ryanmiao -b dc=demo,dc=com "cn=ryan.miao"
# extended LDIF
#
# LDAPv3
# base <dc=demo,dc=com> with scope subtree
# filter: cn=ryan.miao
# requesting: ALL
#

# search result
search: 2
result: 32 No such object

# numResponses: 1

可以确认密码修改成功了,同时也暴露了一个问题,任意一个人都可以 bind 登录,然后查询所有用户的信息。后面我们将关注 acl 权限问题,让每个人只能查询自己的信息,让指定的 group 可以查询所有人的信息。

注意! 我们使用的明文作为密码存储, 这样的传输方式是不推荐的, 可以使用 sha1 来存储。

root@e53c92de7493:~# slappasswd -s ryanmiao
{SSHA}r5yzPeESGLsvX7oxQetVEpel9LhygFef


dn: cn=ryan.miao,ou=后台组,ou=研发部门,dc=demo,dc=com
changetype: modify
replace: userPassword
userPassword: {SSHA}r5yzPeESGLsvX7oxQetVEpel9LhygFef


root@e53c92de7493:~# slappasswd -h {sha} -s ryanmiao
{SHA}vMV4cx3BhPVf0dRvEur3NOWIDEw=

root@e53c92de7493:~# slappasswd -h {md5} -s ryanmiao
{MD5}J3sqNCJFas5wgycX4lJPsg==

# 或者sha1
userPassword: {SHA}vMV4cx3BhPVf0dRvEur3NOWIDEw=

值得注意的是 sha1 的结果并不是通常我们用的 hex 结果,而是通过 utf8 转换的:

public static String sha1(String str)
    throws NoSuchAlgorithmException, UnsupportedEncodingException {
    if (null == str || str.length() == 0) {
        return null;
    }
    MessageDigest mdTemp = MessageDigest.getInstance("SHA1");
    mdTemp.update(str.getBytes("UTF-8"));
    byte[] md = mdTemp.digest();

    return "{SHA}" + Utf8.decode(java.util.Base64.getEncoder().encode(md));
}

Springboot 提供了 LdapShaPasswordEncoder, 但标记为 deprecated, 理由是明文的加密算法不够安全。

我们的 ldap 由于属于同步服务,即 ldap 不负责用户信息的维护,只负责查询。需要由用户中心来同步给 ldap 信息。

这就涉及到密码的问题,用户中心没有存储用户明文的,也就是不能直接同步到ldap。好在可以获得用户密码的 sha,

通过 sha 来同步 ldap 的密码,即 ldap 中的密码是一个 sha 的方式存储的。虽然不够安全,容易被撞,但用着也还行。

如果不信任这种算法,那就不用 ldap。可以使用 oauth 的方式认证第三方系统,大部分系统已支持这种认证方案。

前面提到用户已知个人密码的情况下,如何自己修改密码。

ldappasswd -x -p 389 -D "cn=Barbara Jensen,dc=example,dc=org" -w VSzhfbwA -s 123456

我们先不关注这种行为吧,默认所有第三方系统只有登录权限。关于组织架构的维护,即 ldap 组织的更新,我们采用 其他的方案去管理,ldap 只是用来辅助第三方登录的。

即,其他系统想要修改密码之类的,统一到我们的用户中心服务去修改变更,用户中心负责把信息同步给 ldap。

添加组 Group

有人会问,我之前添加人员的时候添加了很多部门的 ou,不就是 group 吗。

是的,理论上应该是 group。但是由于我们丢了一步,没有设置 ou 的 objectClass 为 group。所以,这里单独讲 group 的故事。

ldap 的 group 是一种单独的类型 objectClass: groupOfNames, 有个字段叫做 member, value 就是 entry 的 dn。如此, 实现了 group-user 的映射关系。

我们可以通过 group 来查询 member,然而,并不能通过 user 直接获取到 group。这在配置第三方系统的时候,没办法做group 认证, 比如 airflow 要求输入 group filter, 默认通过 memberof 的属性值来获取 group。所以,理论上 user 应该有个字段叫做 memberof, value 是 group。

大家可能会觉得 dn 已经很明显的分组了好吧,为啥还要这么复杂。事实上,ldap 也提供了 Reverse Group Membership Maintenance. 由系统来维护二者的映射关系。即

  • group 添加 member 的时候会自动给对应的 entry 添加 memberof 字段
  • 当删除 entry 的时候,也会从 group 里删除 member 字段

这个需要单独配置,默认是不支持的。

添加 memberof 模块

创建 add_module_group.ldif

root@e53c92de7493:~# tee add_module_group.ldif<<EOF
dn: cn=module,cn=config
cn: module
objectClass: olcModuleList
olcModulePath: /usr/lib/ldap

dn: cn=module{0},cn=config
changetype: modify
add: olcModuleLoad
olcModuleLoad: memberof.la
EOF

执行添加

root@e53c92de7493:~# ldapadd -Q -Y EXTERNAL -H ldapi:/// -f add_module_group.ldif
adding new entry "cn=module,cn=config"

modifying entry "cn=module{0},cn=config"

创建 add_group_objectClass.ldif

root@e53c92de7493:~# tee add_group_objectClass.ldif <<EOF
dn: olcOverlay=memberof,olcDatabase={1}mdb,cn=config
objectClass: olcConfig
objectClass: olcMemberOf
objectClass: olcOverlayConfig
objectClass: top
olcOverlay: memberof
olcMemberOfDangling: ignore
olcMemberOfRefInt: TRUE
olcMemberOfGroupOC: groupOfNames
olcMemberOfMemberAD: member
olcMemberOfMemberOfAD: memberOf
EOF

执行添加

root@e53c92de7493:~# ldapadd -Q -Y EXTERNAL -H ldapi:/// -f add_group_objectClass.ldif

adding new entry "olcOverlay=memberof,olcDatabase={1}mdb,cn=config"

添加一个 group

创建 addgroup.ldif

root@e53c92de7493:~# tee addgroup.ldif <<EOF
dn: olcOverlay=memberof,olcDatabase={1}mdb,cn=config
objectClass: olcConfig
objectClass: olcMemberOf
objectClass: olcOverlayConfig
objectClass: top
olcOverlay: memberof
olcMemberOfDangling: ignore
olcMemberOfRefInt: TRUE
olcMemberOfGroupOC: groupOfNames
olcMemberOfMemberAD: member
olcMemberOfMemberOfAD: memberOf
root@e53c92de7493:~# cat addgroup.ldif 
dn: cn=g-admin,ou=Group,dc=demo,dc=com
objectClass: groupOfNames
cn: g-admin
member: cn=ryan.miao,ou=后台组,ou=研发部门,ou=People,dc=demo,dc=com
member: cn=hr-ryan,ou=HR,ou=People,dc=demo,dc=com
EOF

执行添加

root@e53c92de7493:~# ldapmodify -a -D "cn=admin,dc=demo,dc=com" -w admin -f addgroup.ldif

adding new entry "cn=g-admin,ou=Group,dc=demo,dc=com"

查看组

root@e53c92de7493:~# ldapsearch -x -D "cn=admin,dc=demo,dc=com" -w admin -b "ou=Group,c=com"
# extended LDIF
#
# LDAPv3
# base <ou=Group,dc=demo,dc=com> with scope subtree
# filter: (objectclass=*)
# requesting: ALL
#

# Group, demo.com
dn: ou=Group,dc=demo,dc=com
objectClass: organizationalUnit
ou: Group

# g-admin, Group, demo.com
dn: cn=g-admin,ou=Group,dc=demo,dc=com
objectClass: groupOfNames
cn: g-admin
member:: Y249cnlhbi5taWFvLG91PeWQjuWPsOe7hCxvdT3noJTlj5Hpg6jpl6gsb3U9UGVvcGxlL
 GRjPWRlbW8sZGM9Y29t
member: cn=hr-ryan,ou=HR,ou=People,dc=demo,dc=com

# search result
search: 2
result: 0 Success

# numResponses: 3
# numEntries: 2

再来查看 entry 是否添加了 memberof, ldapsearch 当不指定字段的时候,默认返回全部强制字段,memberof 不属于强制,需要单独指明!

root@e53c92de7493:~# ldapsearch -x -D "cn=admin,dc=demo,dc=com" -w admin -b "ou=People,c=com" "(|(cn=ryan.miao)(cn=hr-*))" memberof
# extended LDIF
#
# LDAPv3
# base <ou=People,dc=demo,dc=com> with scope subtree
# filter: (|(cn=ryan.miao)(cn=hr-*))
# requesting: memberof
#

# hr-ryan, HR, People, demo.com
dn: cn=hr-ryan,ou=HR,ou=People,dc=demo,dc=com
memberOf: cn=g-admin,ou=Group,dc=demo,dc=com

# ryan.miao, \E5\90\8E\E5\8F\B0\E7\BB\84, \E7\A0\94\E5\8F\91\E9\83\A8\E9\97\A8,
  People, demo.com
dn:: Y249cnlhbi5taWFvLG91PeWQjuWPsOe7hCxvdT3noJTlj5Hpg6jpl6gsb3U9UGVvcGxlLGRjP
 WRlbW8sZGM9Y29t
memberOf: cn=g-admin,ou=Group,dc=demo,dc=com

# search result
search: 2
result: 0 Success

# numResponses: 3
# numEntries: 2

可以看到,这两个人都 link 到了 admin 组。如此实现了我们的组添加和管理。

同时,再次引入了新的查询语法,filter 的正则匹配。

  • (|(cn=ryan.miao)(cn=hr-*)) 表示或者满足某个条件,这里就是为了查询这两个人,另外 * 表示通配符
  • (&(objectClass=inetOrgPerson)(cn=ryan.miao)) 第三方系统,比如 Python 集成 ldap 的配置,通常会有一个 basedn, 就是我们的域名了,然后 userfilter,这个 filter 就是这个。

我们通常填写 objectClass=inetOrgPerson。然后让我们配置 user_name_attr, 这就是唯一属性,我们说我们的 cn 唯一。

所以,一个 Python 的 ldap 配置,通常是这个样子的。

[ldap]
# set this to ldaps://<your.ldap.server>:<port>
uri = ldap://172.17.0.2:389
user_filter = objectClass=inetOrgPerson
user_name_attr = cn
group_member_attr = memberof
superuser_filter =
data_profiler_filter =
bind_user = cn=admin,dc=demo,dc=com
bind_password = admin
basedn = dc=demo,dc=com
cacert =
search_scope = SUBTREE

添加用户到 group

我们来创建一个 common group, 表示所有人都应该在的一个 group。

root@e53c92de7493:~# tee commongroup.ldif <<EOF
dn: cn=g-users,ou=Group,dc=demo,dc=com
objectClass: groupOfNames
cn: g-users
member: cn=ryan.miao,ou=后台组,ou=研发部门,ou=People,dc=demo,dc=com
member: cn=hr-ryan,ou=HR,ou=People,dc=demo,dc=com
EOF

# 执行添加
root@e53c92de7493:~# ldapmodify -a -D "cn=admin,dc=demo,dc=com" -w admin -f commongroup.ldif

adding new entry "cn=g-users,ou=Group,dc=demo,dc=com"

到目前为止,我们添加了 2 个 group:

root@e53c92de7493:~# ldapsearch -D "cn=admin,dc=demo,dc=com" -w admin -b "dc=demo,s sub "objectClass=groupOfNames" dn member
# extended LDIF
#
# LDAPv3
# base <dc=demo,dc=com> with scope subtree
# filter: objectClass=groupOfNames
# requesting: dn member
#

# g-admin, Group, demo.com
dn: cn=g-admin,ou=Group,dc=demo,dc=com
member:: Y249cnlhbi5taWFvLG91PeWQjuWPsOe7hCxvdT3noJTlj5Hpg6jpl6gsb3U9UGVvcGxlL
 GRjPWRlbW8sZGM9Y29t
member: cn=hr-ryan,ou=HR,ou=People,dc=demo,dc=com

# g-users, Group, demo.com
dn: cn=g-users,ou=Group,dc=demo,dc=com
member:: Y249cnlhbi5taWFvLG91PeWQjuWPsOe7hCxvdT3noJTlj5Hpg6jpl6gsb3U9UGVvcGxlL
 GRjPWRlbW8sZGM9Y29t
member: cn=hr-ryan,ou=HR,ou=People,dc=demo,dc=com

# search result
search: 2
result: 0 Success

# numResponses: 3
# numEntries: 2

接下来,我们把剩下的用户加入到 g-users 这个 group, 以后所有人加入的默认 group。

创建 addUserToGroup.ldif

root@e53c92de7493:~# tee addUserToGroup.ldif <<EOF
dn: cn=g-users,ou=Group,dc=demo,dc=com
objectClass: groupOfNames
cn: g-users
member: cn=ryan.miao,ou=后台组,ou=研发部门,ou=People,dc=demo,dc=com
member: cn=hr-ryan,ou=HR,ou=People,dc=demo,dc=com
root@e53c92de7493:~# cat addUserToGroup.ldif 
dn: cn=g-users,ou=Group,dc=demo,dc=com
changetype: modify
add: member
member: cn=fang.huang,ou=HR,ou=People,dc=demo,dc=com
member: cn=someone,ou=后台组,ou=研发部门,ou=People,dc=demo,dc=com
member: cn=tester.miao,ou=测试组,ou=研发部门,ou=People,dc=demo,dc=com
EOF

# 执行添加
root@e53c92de7493:~# ldapmodify -x -D "cn=admin,dc=demo,dc=com" -w admin -f addUserToGroup.ldif

modifying entry "cn=g-users,ou=Group,dc=demo,dc=com"

查看

root@e53c92de7493:~# ldapsearch -D "cn=admin,dc=demo,dc=com" -w admin -b "dc=demo,s sub "objectClass=groupOfNames"
# extended LDIF
#
# LDAPv3
# base <dc=demo,dc=com> with scope subtree
# filter: objectClass=groupOfNames
# requesting: ALL
#

# g-admin, Group, demo.com
dn: cn=g-admin,ou=Group,dc=demo,dc=com
objectClass: groupOfNames
cn: g-admin
member:: Y249cnlhbi5taWFvLG91PeWQjuWPsOe7hCxvdT3noJTlj5Hpg6jpl6gsb3U9UGVvcGxlL
 GRjPWRlbW8sZGM9Y29t
member: cn=hr-ryan,ou=HR,ou=People,dc=demo,dc=com

# g-users, Group, demo.com
dn: cn=g-users,ou=Group,dc=demo,dc=com
objectClass: groupOfNames
cn: g-users
member:: Y249cnlhbi5taWFvLG91PeWQjuWPsOe7hCxvdT3noJTlj5Hpg6jpl6gsb3U9UGVvcGxlL
 GRjPWRlbW8sZGM9Y29t
member: cn=hr-ryan,ou=HR,ou=People,dc=demo,dc=com
member: cn=fang.huang,ou=HR,ou=People,dc=demo,dc=com
member:: Y249c29tZW9uZSxvdT3lkI7lj7Dnu4Qsb3U956CU5Y+R6YOo6ZeoLG91PVBlb3BsZSxkY
 z1kZW1vLGRjPWNvbQ==
member:: Y249dGVzdGVyLm1pYW8sb3U95rWL6K+V57uELG91PeeglOWPkemDqOmXqCxvdT1QZW9wb
 GUsZGM9ZGVtbyxkYz1jb20=

# search result
search: 2
result: 0 Success

# numResponses: 3
# numEntries: 2

从 Group 中移除 user

g-admin 是一个管理员分组,我们去掉普通用户

cn=ryan.miao,ou=后台组,ou=研发部门,ou=People,dc=demo,dc=com

创建 removeUserFromGroup.ldif

root@e53c92de7493:~# tee removeUserFromGroup.ldif <<EOF
dn: cn=g-admin,ou=Group,dc=demo,dc=com
changetype: modify
delete: member
member: cn=ryan.miao,ou=后台组,ou=研发部门,ou=People,dc=demo,dc=com

# 执行删除
root@e53c92de7493:~# ldapmodify -x -D "cn=admin,dc=demo,dc=com" -w admin -f removeUserFromGroup.ldif

modifying entry "cn=g-admin,ou=Group,dc=demo,dc=com"

最终 Group 和 user 的关系

group 可以有多个 user, user 可以归属于多个 group,是多对多的关系。

group 有多个 member 字段, user 有多个 memberof 字段。

ACL权限控制

Access Control List (ACL) 表示权限控制。从前面的测试可以看到,默认是没开启权限的。任何人都可以连接查询和操作。

acl 的设置方式很多,鉴于我们并没有将 ldap 作为主要的数据存储方案,即不做过多的权限设置了,只要关掉匿名访问,只允许 read,允许个人修改个人信息就好了。更多设置方案可以参照官网。

acl 的配置文件

配置文件还是开始提到的,我们可以查看现有的配置:

root@e53c92de7493:~# ldapsearch -Y EXTERNAL -H ldapi:/// -b cn=config dn
SASL/EXTERNAL authentication started
SASL username: gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth
SASL SSF: 0
# extended LDIF
#
# LDAPv3
# base <cn=config> with scope subtree
# filter: (objectclass=*)
# requesting: dn
#

# config
dn: cn=config

# module{0}, config
dn: cn=module{0},cn=config

# module{1}, config
dn: cn=module{1},cn=config

# schema, config
dn: cn=schema,cn=config

# {0}core, schema, config
dn: cn={0}core,cn=schema,cn=config

# {1}cosine, schema, config
dn: cn={1}cosine,cn=schema,cn=config

# {2}nis, schema, config
dn: cn={2}nis,cn=schema,cn=config

# {3}inetorgperson, schema, config
dn: cn={3}inetorgperson,cn=schema,cn=config

# {4}ppolicy, schema, config
dn: cn={4}ppolicy,cn=schema,cn=config

# {5}dhcp, schema, config
dn: cn={5}dhcp,cn=schema,cn=config

# {6}dnszone, schema, config
dn: cn={6}dnszone,cn=schema,cn=config

# {7}mail, schema, config
dn: cn={7}mail,cn=schema,cn=config

# {8}mmc, schema, config
dn: cn={8}mmc,cn=schema,cn=config

# {9}openssh-lpk, schema, config
dn: cn={9}openssh-lpk,cn=schema,cn=config

# {10}quota, schema, config
dn: cn={10}quota,cn=schema,cn=config

# {11}radius, schema, config
dn: cn={11}radius,cn=schema,cn=config

# {12}samba, schema, config
dn: cn={12}samba,cn=schema,cn=config

# {13}zarafa, schema, config
dn: cn={13}zarafa,cn=schema,cn=config

# {-1}frontend, config
dn: olcDatabase={-1}frontend,cn=config

# {0}config, config
dn: olcDatabase={0}config,cn=config

# {1}mdb, config
dn: olcDatabase={1}mdb,cn=config

# {0}memberof, {1}mdb, config
dn: olcOverlay={0}memberof,olcDatabase={1}mdb,cn=config

# {1}refint, {1}mdb, config
dn: olcOverlay={1}refint,olcDatabase={1}mdb,cn=config

# {2}memberof, {1}mdb, config
dn: olcOverlay={2}memberof,olcDatabase={1}mdb,cn=config

# search result
search: 2
result: 0 Success

# numResponses: 25
# numEntries: 24

acl 就在 dn: olcDatabase={1}mdb,cn=config, 我们可以查看具体的配置:

root@e53c92de7493:~# ldapsearch -Y EXTERNAL -H ldapi:/// -b cn=config 'olcDatabas
SASL/EXTERNAL authentication started
SASL username: gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth
SASL SSF: 0
# extended LDIF
#
# LDAPv3
# base <cn=config> with scope subtree
# filter: olcDatabase={1}mdb
# requesting: ALL
#

# {1}mdb, config
dn: olcDatabase={1}mdb,cn=config
objectClass: olcDatabaseConfig
objectClass: olcMdbConfig
olcDatabase: {1}mdb
olcDbDirectory: /var/lib/ldap
olcAccess: {0}to attrs=userPassword,shadowLastChange by self write by dn="cn=a
 dmin,dc=example,dc=org" write by anonymous auth by * none
olcAccess: {1}to * by self read by dn="cn=admin,dc=example,dc=org" write by *
 none
olcLastMod: TRUE
olcDbCheckpoint: 512 30
olcDbIndex: uid eq
olcDbIndex: mail eq
olcDbIndex: memberOf eq
olcDbIndex: entryCSN eq
olcDbIndex: entryUUID eq
olcDbIndex: objectClass eq
olcDbMaxSize: 1073741824
olcRootDN: cn=admin,dc=demo,dc=com
olcSuffix: dc=demo,dc=com
olcRootPW: {SSHA}JabzOmn1ZJW4ShSpUThSAKpogzix+eYu

# search result
search: 2
result: 0 Success

# numResponses: 2
# numEntries: 1

接下来,使用 modify 添加 acl 即可。

创建 addacl.ldif

root@e53c92de7493:~# tee addacl.ldif <<EOF
dn: olcDatabase={1}mdb,cn=config
changetype: modify
# 只有自己可以修改密码,不允许匿名访问,允许 g-admin 组修改
add: olcAccess
olcAccess: {0}to attrs=userPassword by self write by anonymous auth by group.exact="cn=g-admin,ou=Group,dc=demo,dc=com" write by * none
-
# 自己可以修改自己的信息,g-admin 可以修改任何信息
add: olcAccess
olcAccess: {1}to * by self write by group.exact="cn=g-admin,ou=Group,dc=demo,dc=com" write by * none
EOF

执行

root@e53c92de7493:~# ldapmodify -H ldapi:/// -Y EXTERNAL -f addacl.ldif

SASL/EXTERNAL authentication started
SASL username: gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth
SASL SSF: 0
modifying entry "olcDatabase={1}mdb,cn=config"

验证权限

添加一个普通用户:

root@e53c92de7493:~# tee addtwo.ldif <<EOF
dn: cn=hr-miao,ou=HR,ou=People,dc=demo,dc=com
changetype: add
objectClass: inetOrgPerson
cn: hr-miao
userPassword: 123456
departmentNumber: 3
sn: hr-miao
title: HRBP
mail: hr-miao@demo.com
uid: 10006
displayName: 我是 HR
EOF

# 执行添加
root@e53c92de7493:~# ldapmodify -a -H ldapi:/// -D "cn=admin,dc=demo,dc=com" -w admin -f addtwo.ldif

adding new entry "cn=hr-miao,ou=HR,ou=People,dc=demo,dc=com"

现在我们有两个用户来比较

  • cn=hr-miao,ou=HR,ou=People,dc=demo,dc=com 是普通用户
  • cn=hr-ryan,ou=HR,ou=People,dc=demo,dc=com 是g-admin用户

分别来查询:

root@e53c92de7493:~# ldapsearch -H ldapi:/// -x -D "cn=hr-ryan,ou=HR,ou=People,dc=demo,w 123456 -b "dc=demo,dc=com" "cn=hr-ryan" dn memberof
# extended LDIF
#
# LDAPv3
# base <dc=demo,dc=com> with scope subtree
# filter: cn=hr-ryan
# requesting: dn memberof
#

# hr-ryan, HR, People, demo.com
dn: cn=hr-ryan,ou=HR,ou=People,dc=demo,dc=com
memberOf: cn=g-admin,ou=Group,dc=demo,dc=com
memberOf: cn=g-users,ou=Group,dc=demo,dc=com

# search result
search: 2
result: 0 Success

# numResponses: 2
# numEntries: 1

root@e53c92de7493:~# ldapsearch -H ldapi:/// -x -D "cn=hr-miao,ou=HR,ou=People,dc=demo,w 123456 -b "dc=demo,dc=com" "cn=hr-miao" dn memberof
# extended LDIF
#
# LDAPv3
# base <dc=demo,dc=com> with scope subtree
# filter: cn=hr-miao
# requesting: dn memberof
#

# search result
search: 2
result: 32 No such object

# numResponses: 1

可以看到,g-admin 成员可以查询其他所有, 普通用户只能连接。

比较更新密码能力:

root@e53c92de7493:~# tee updateselfpass.ldif <<EOF
dn: cn=ryan.miao,ou=后台组,ou=研发部门,ou=People,dc=demo,dc=com
changetype: modify
replace: userPassword
userPassword: ryanmiao
EOF

# 执行更改密码
## 修改自己的密码 ok
root@e53c92de7493:~# ldapmodify -H ldapi:/// -x -D "cn=hr-miao,ou=HR,ou=People,dc=demo,w 123456 -f updateselfpass.ldif
modifying entry "cn=hr-miao,ou=HR,ou=People,dc=demo,dc=com"



## 确认密码被修改
root@e53c92de7493:~# ldapmodify -H ldapi:/// -x -D "cn=hr-miao,ou=HR,ou=People,dc=demo,w 123456 -f updateselfpass.ldif
ldap_bind: Invalid credentials (49)

尝试修改其他人密码

root@e53c92de7493:~# sed -i "1c dn: cn=hr-ryan,ou=HR,ou=People,dc=demo,dc=com" updateselfpass.ldif

## 修改 dn 为别人后不能修改密码
root@e53c92de7493:~# ldapmodify -H ldapi:/// -x -D "cn=hr-miao,ou=HR,ou=People,dc=demo,w ryanmiao -f updateselfpass.ldif
modifying entry "cn=hr-ryan,ou=HR,ou=People,dc=demo,dc=com"
ldap_modify: Insufficient access (50)

三、总结 ldap 命令

ldap 主要命令有:

  • ldapadd
  • ldapmodify
  • ldapsearch

我们用到的操作项有

option 含义
-H ldap server 地址, 可以是 ldap://192.168.12.18:389 表示tcp, 可以是 ldap:/// 表示本地的 tcp, 可以是ldapi:/// 本地 unix socket 连接
-x 启用简单认证,通过 -D dn -w 密码的方式认证
-f 指定要修改的文件
-a 使用 ldapmodify 增加一个 entry 的时候等同于 ldapadd
-b basedn 根目录, 将在此目录下查询
-Y EXTERNAL 本地执行,修改配置文件,比如 basedn, rootdn,rootpw,acl, module 等信息

ldapadd

添加一个 entry. 可以添加 schema 配置

ldapadd -Y EXTERNAL -H ldapi:/// -f /etc/openldap/schema/cosine.ldif

添加额外的 module

ldapadd -Q -Y EXTERNAL -H ldapi:/// -f add_module_group.sh

添加普通 entry

ldapadd -x -D cn=admin,dc=demo,dc=com -w admin -f base.ldif

ldapmodify

修改 entry, 可以更新配置信息

ldapmodify -Q -Y EXTERNAL -H ldapi:/// -f tmp.ldif

对应的更新文件语法

dn: 要更新的 entry的dn, 配置为 olcDatabase={2}hdb,cn=config, 用户为用户 dn
changetype: modify
replace: olcRootDN   replace替换的字段
olcRootDN: cn=admin,dc=demo,dc=com
-
replace: olcSuffix
olcSuffix: dc=demo,dc=com
-
add: olcRootPW   add添加新的字段
olcRootPW: <pass>

ldapsearch

search 查询,主要集中在 filter 的使用上。

ldapsearch -H ldapi:/// -D cn=admin,cn=demo,cn=com -w admin -s sub "filter" attr

  • -s scope 指定查询范围, 有 base one sub children 主要用 sub 表示 base 之下的所有子目录。对应 Python 里的 SUBTREE
  • filter 语法,正则语法。因为使用的时候传递过来的通常是 username, 需要比较 username 在 ldap 中的字段, 比如
(|(cn=Steve*)(sn=Steve*)(mail=Steve*)(givenName=Steve*)(uid=Steve*))

attr 要返回的字段, 必须返回的字段可以在配置文件里查看。memberof 非必须。

四、通过 phpldapadmin 链接 openldap

确保 phpldapadmin 容器服务已启动

[root@openldap]# docker ps
CONTAINER ID        IMAGE                        COMMAND                 CREATED             STATUS              PORTS                                        NAMES
0426cb9e1ec4        osixia/phpldapadmin:latest   "/container/tool/run"   23 hours ago        Up 4 seconds        443/tcp, 0.0.0.0:8080->80/tcp                phpldapadmin
e53c92de7493        osixia/openldap:1.3.0        "/container/tool/run"   4 days ago          Up 6 hours          0.0.0.0:389->389/tcp, 0.0.0.0:636->636/tcp   ldap

通过浏览器访问:

  • URL: http://hostIP:8080
  • User: cn=admin,dc=demo,dc=com
  • Passwd: admin

五、ACL 附录

olcAccess: <access directive>
    <access directive> ::= to <what>
        [by <who> [<access>] [<control>] ]+
    <what> ::= * |
        [dn[.<basic-style>]=<regex> | dn.<scope-style>=<DN>]
        [filter=<ldapfilter>] [attrs=<attrlist>]
    <basic-style> ::= regex | exact
    <scope-style> ::= base | one | subtree | children
    <attrlist> ::= <attr> [val[.<basic-style>]=<regex>] | <attr> , <attrlist>
    <attr> ::= <attrname> | entry | children
    <who> ::= * | [anonymous | users | self
            | dn[.<basic-style>]=<regex> | dn.<scope-style>=<DN>]
        [dnattr=<attrname>]
        [group[/<objectclass>[/<attrname>][.<basic-style>]]=<regex>]
        [peername[.<basic-style>]=<regex>]
        [sockname[.<basic-style>]=<regex>]
        [domain[.<basic-style>]=<regex>]
        [sockurl[.<basic-style>]=<regex>]
        [set=<setspec>]
        [aci=<attrname>]
    <access> ::= [self]{<level>|<priv>}
    <level> ::= none | disclose | auth | compare | search | read | write | manage
    <priv> ::= {=|+|-}{m|w|r|s|c|x|d|0}+
    <control> ::= [stop | continue | break]

关于 acl 中的 who

Table 6.3: Access Entity Specifiers

Specifier Entities
* All, including anonymous and authenticated users
anonymous Anonymous (non-authenticated) users
users Authenticated users
self User associated with target entry
dn[.]= Users matching a regular expression
dn.= Users within scope of a DN

关于 dn 的授权

For example, if the directory contained entries named:

    0: o=suffix
    1: cn=Manager,o=suffix
    2: ou=people,o=suffix
    3: uid=kdz,ou=people,o=suffix
    4: cn=addresses,uid=kdz,ou=people,o=suffix
    5: uid=hyc,ou=people,o=suffix
Then:

dn.base="ou=people,o=suffix" match 2;
dn.one="ou=people,o=suffix" match 3, and 5;
dn.subtree="ou=people,o=suffix" match 2, 3, 4, and 5; and
dn.children="ou=people,o=suffix" match 3, 4, and 5.

一个acl示例

# ACL1
access to attrs=userpassword
       by self       write
       by anonymous  auth
       by group.exact="cn=itpeople,ou=groups,dc=example,dc=com"
                     write
       by *          none
# ACL2
access to attrs=carlicense,homepostaladdress,homephone
       by self       write
       by group.exact="cn=hrpeople,ou=groups,dc=example,dc=com"
                     write
       by *          none
# ACL3
access to *
       by self       write
       by group.exact="cn=hrpeople,ou=groups,dc=example,dc=com"
                     write
       by users      read
       by *          none

参考

Search

    Table of Contents