ldap3 是一个严格遵守 RFC 4510 规范,完全由纯 Python 代码实现的 LDAPv3 客户端。
ldap3 只依赖于 Python 标准库和pyasn1,无需使用 C 编译器编译或者安装其他二进制程序,直接使用pip命令安装即可(pip install ldap3)。
一、连接服务器
1 | from ldap3 import Server, Connection, ALL |
或者也可以使用更简短的形式:1
2
3conn = Connection('<hostname_or_ip>', auto_bind=True)
conn
Connection(server=Server(host='xx.xx.xx.xx', port=389, use_ssl=False, allowed_referral_hosts=[('*', True)], get_info='SCHEMA', mode='IP_V6_PREFERRED'), auto_bind='NO_TLS', version=3, authentication='ANONYMOUS', client_strategy='SYNC', auto_referrals=True, check_names=True, read_only=False, lazy=False, raise_exceptions=False, fast_decoder=True, auto_range=True, return_empty_attributes=True, auto_encode=True, auto_escape=True, use_referral_cache=False)
获取服务器信息:1
2
3
4
5
6
7
8
9
10server = Server('<hostname_or_ip>', get_info=ALL)
conn = Connection(server, auto_bind=True)
server.info
DSA info (from DSE):
Supported LDAP versions: 3, 2
Naming contexts:
DC=example,DC=com
CN=Configuration,DC=example,DC=com
CN=Schema,CN=Configuration,DC=example,DC=com
...
用户绑定
1 | from ldap3 import Server, Connection, ALL |
PS:通常在对域账号进行修改等操作时,需要启用 SSL 连接以符合安全规范。在构造 Server 对象时传入 use_ssl=True 即可:1
server = Server('<hostname_or_ip>', port=636, use_ssl=True, get_info=ALL)
如有需要,TLS 的启用可以参考官方文档 https://ldap3.readthedocs.io/ssltls.html
二、对象检索
使用 FreeIPA 开放的 LDAP 示例服务测试搜索操作:1
2
3
4
5
6
7
8
9
10
11from ldap3 import Server, Connection, ALL
server = Server('ipa.demo1.freeipa.org', get_info=ALL)
conn = Connection(server, 'uid=admin,cn=users,cn=accounts,dc=demo1,dc=freeipa,dc=org', 'Secret123', auto_bind=True)
conn.search('dc=demo1,dc=freeipa,dc=org', '(objectclass=person)')
True
conn.entries
[DN: uid=admin,cn=users,cn=accounts,dc=demo1,dc=freeipa,dc=org - STATUS: Read - READ TIME: 2020-01-13T18:57:39.837356
, DN: uid=manager,cn=users,cn=accounts,dc=demo1,dc=freeipa,dc=org - STATUS: Read - READ TIME: 2020-01-13T18:57:39.837594
, DN: uid=employee,cn=users,cn=accounts,dc=demo1,dc=freeipa,dc=org - STATUS: Read - READ TIME: 2020-01-13T18:57:39.837722
, DN: uid=helpdesk,cn=users,cn=accounts,dc=demo1,dc=freeipa,dc=org - STATUS: Read - READ TIME: 2020-01-13T18:57:39.837845
]
search 方法有两个参数是必需的:
search_base用于指定搜索的起始位置search_filter用于指定搜索的筛选条件
filter 的定义语法支持 =、<=、>= 等比较运算符和 &、|、! 等逻辑运算符。如:(&(givenName=John)(mail=*@example.com)) 表示寻找名字为 John 且邮箱以 ``@example.com 结尾的域账号。
以下搜索条件则表示寻找名字为 Fred 或 John,并且邮箱以 ``@example.com 结尾的域账号:1
2
3
4
5
6
7(&
(|
(givenName=Fred)
(givenName=John)
)
(mail=*@example.com)
)
attributes
参考如下代码:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16conn.search('dc=demo1,dc=freeipa,dc=org', '(&(objectclass=person)(uid=admin))', attributes=['sn', 'krbLastPwdChange', 'objectclass'])
True
conn.entries[0]
DN: uid=admin,cn=users,cn=accounts,dc=demo1,dc=freeipa,dc=org - STATUS: Read - READ TIME: 2020-01-13T19:14:20.657761
krbLastPwdChange: 2019-01-25 15:16:11+00:00
objectclass: top
person
posixaccount
krbprincipalaux
krbticketpolicyaux
inetuser
ipaobject
ipasshuser
ipaSshGroupOfPubKeys
ipaNTUserAttrs
sn: Administrator
其中 (&(objectclass=person)(uid=admin) 用于指定查找对象类型为 person 且 uid 为 admin 的用户账号; attributes 参数则用于指定搜索结果中额外包含该账户的 sn、krbLastPwdChange 和 objectclass 属性。
PS:设置 attributes = ['*'] 可以在搜索结果中显示对象的所有属性。
可以使用 Entry 对象的 entry_to_json 方法将该对象的所有属性以 JSON 格式输出:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24print(conn.entries[0].entry_to_json())
{
"attributes": {
"krbLastPwdChange": [
"2019-01-25 15:16:11+00:00"
],
"objectclass": [
"top",
"person",
"posixaccount",
"krbprincipalaux",
"krbticketpolicyaux",
"inetuser",
"ipaobject",
"ipasshuser",
"ipaSshGroupOfPubKeys",
"ipaNTUserAttrs"
],
"sn": [
"Administrator"
]
},
"dn": "uid=admin,cn=users,cn=accounts,dc=demo1,dc=freeipa,dc=org"
}
此外还可以使用 paged_size 参数控制每页显示的结果数量。综合实例如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16searchParameters = {'search_base': 'dc=demo1,dc=freeipa,dc=org',
'search_filter': '(objectClass=Person)',
'attributes': ['cn', 'givenName'],
'paged_size': 3}
conn.search(**searchParameters)
True
conn.entries
[DN: uid=admin,cn=users,cn=accounts,dc=demo1,dc=freeipa,dc=org - STATUS: Read - READ TIME: 2020-01-13T19:23:50.396926
cn: Administrator
, DN: uid=manager,cn=users,cn=accounts,dc=demo1,dc=freeipa,dc=org - STATUS: Read - READ TIME: 2020-01-13T19:23:50.397214
cn: Test Manager
givenName: Test
, DN: uid=employee,cn=users,cn=accounts,dc=demo1,dc=freeipa,dc=org - STATUS: Read - READ TIME: 2020-01-13T19:23:50.397509
cn: Test Employee
givenName: Test
]
三、数据库操作
创建条目
新建 OU:1
2conn.add('ou=ldap3-tutorial,dc=demo1,dc=freeipa,dc=org', 'organizationalUnit')
True
新建用户:1
2conn.add('cn=b.young,ou=ldap3-tutorial,dc=demo1,dc=freeipa,dc=org', 'inetOrgPerson', {'givenName': 'Beatrix', 'sn': 'Young', 'departmentNumber': 'DEV', 'telephoneNumber': 1111})
True
查看 objectClass 结构:1
2
3
4
5
6
7
8
9
10server.schema.object_classes['person']
Object class: 2.5.6.6
Short name: person
Type: Structural
Superior: top
Must contain attributes: sn, cn
May contain attributes: userPassword, telephoneNumber, seeAlso, description
Extensions:
X-ORIGIN: RFC 4519
OidInfo: ('2.5.6.6', 'OBJECT_CLASS', 'person', 'RFC4519')
重命名条目
1 | conn.modify_dn('cn=b.young,ou=ldap3-tutorial,dc=demo1,dc=freeipa,dc=org', 'cn=b.smith') |
移动条目
1 | conn.add('ou=moved,ou=ldap3-tutorial,dc=demo1,dc=freeipa,dc=org','organizationalUnit') |
更新条目
添加属性值:1
2
3
4
5
6
7
8
9
10from ldap3 import MODIFY_ADD, MODIFY_REPLACE, MODIFY_DELETE
conn.modify('cn=b.smith,ou=moved,ou=ldap3-tutorial,dc=demo1,dc=freeipa,dc=org', {'sn': [(MODIFY_ADD, ['Smyth'])]})
True
conn.search('ou=moved,ou=ldap3-tutorial,dc=demo1,dc=freeipa,dc=org', '(cn=b.smith)', attributes=['cn', 'sn'])
True
conn.entries[0]
DN: cn=b.smith,ou=moved,ou=ldap3-tutorial,dc=demo1,dc=freeipa,dc=org - STATUS: Read - READ TIME: 2020-01-14T13:50:45.461241
cn: b.smith
sn: Young
Smyth
移除属性值:1
2
3
4
5
6
7
8conn.modify('cn=b.smith,ou=moved,ou=ldap3-tutorial,dc=demo1,dc=freeipa,dc=org', {'sn': [(MODIFY_DELETE, ['Young'])]})
True
conn.search('ou=moved,ou=ldap3-tutorial,dc=demo1,dc=freeipa,dc=org', '(cn=b.smith)', attributes=['cn', 'sn'])
True
conn.entries[0]
DN: cn=b.smith,ou=moved,ou=ldap3-tutorial,dc=demo1,dc=freeipa,dc=org - STATUS: Read - READ TIME: 2020-01-14T13:52:43.586339
cn: b.smith
sn: Smyth
替换属性值:1
2
3
4
5
6
7
8conn.modify('cn=b.smith,ou=moved,ou=ldap3-tutorial,dc=demo1,dc=freeipa,dc=org', {'sn': [(MODIFY_REPLACE, ['Smith'])]})
True
conn.search('ou=moved,ou=ldap3-tutorial,dc=demo1,dc=freeipa,dc=org', '(cn=b.smith)', attributes=['cn', 'sn'])
True
conn.entries[0]
DN: cn=b.smith,ou=moved,ou=ldap3-tutorial,dc=demo1,dc=freeipa,dc=org - STATUS: Read - READ TIME: 2020-01-14T13:53:28.834019
cn: b.smith
sn: Smith
判断属性是否为某个特定值:1
2
3
4conn.compare('cn=b.smith,ou=moved,ou=ldap3-tutorial,dc=demo1,dc=freeipa,dc=org', 'departmentNumber', 'DEV')
True
conn.compare('cn=b.smith,ou=moved,ou=ldap3-tutorial,dc=demo1,dc=freeipa,dc=org', 'departmentNumber', 'QA')
False
四、域账号操作
修改账户密码:1
2
3
4
5
6
7
8
9
10from ldap3 import Server, Connection
server = Server('<hostname_or_ip', use_ssl=True)
conn = Connection(server, user='admin@example.com', password='admin@123', auto_bind=True)
conn.search('ou=Domain Users,dc=example,dc=com', '(&(objectClass=person)(sAMAccountName=admin))')
True
dn = conn.entries[0].entry_dn
dn
'CN=admin,OU=Domain Users,DC=example,DC=com'
conn.extend.microsoft.modify_password(dn, 'new_password')
True
解锁账户:1
2conn.extend.microsoft.unlock_account(dn)
True
此外该模块下还有 addMembersToGroups 和 removeMembersFromGroups 等函数。