Python 通过 ldap3 操作 Windows 域账号

ldap3 是一个严格遵守 RFC 4510 规范,完全由纯 Python 代码实现的 LDAPv3 客户端。
ldap3 只依赖于 Python 标准库和 pyasn1,无需使用 C 编译器编译或者安装其他二进制程序,直接使用 pip 命令安装即可(pip install ldap3)。

一、连接服务器

1
2
3
4
5
>>> from ldap3 import Server, Connection, ALL
>>> server = Server('<hostname_or_ip>')
>>> conn = Connection(server)
>>> conn.bind()
True

或者也可以使用更简短的形式:

1
2
3
>>> conn = 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
10
>>> server = 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
2
3
4
5
>>> from ldap3 import Server, Connection, ALL
>>> server = Server('<hostname_or_ip>', get_info=ALL)
>>> conn = Connection(server, user='admin@example.com', password='admin@123', auto_bind=True)
>>> conn.extend.standard.who_am_i()
'u:EXAMPLE\\admin'

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
11
>>> from 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 结尾的域账号。

以下搜索条件则表示寻找名字为 FredJohn,并且邮箱以 ``@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
16
>>> conn.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) 用于指定查找对象类型为 personuidadmin 的用户账号; attributes 参数则用于指定搜索结果中额外包含该账户的 snkrbLastPwdChangeobjectclass 属性。

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
24
>>> print(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
16
>>> searchParameters = {'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
2
>>> conn.add('ou=ldap3-tutorial,dc=demo1,dc=freeipa,dc=org', 'organizationalUnit')
True

新建用户:

1
2
>>> conn.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
10
>>> server.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
2
>>> conn.modify_dn('cn=b.young,ou=ldap3-tutorial,dc=demo1,dc=freeipa,dc=org', 'cn=b.smith')
True
移动条目
1
2
3
4
>>> conn.add('ou=moved,ou=ldap3-tutorial,dc=demo1,dc=freeipa,dc=org','organizationalUnit')
True
>>> conn.modify_dn('cn=b.smith,ou=ldap3-tutorial,dc=demo1,dc=freeipa,dc=org', 'cn=b.smith', new_superior='ou=moved, ou=ldap3-tutorial,dc=demo1,dc=freeipa,dc=org')
True
更新条目

添加属性值:

1
2
3
4
5
6
7
8
9
10
>>> from 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
8
>>> conn.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
8
>>> conn.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
4
>>> conn.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
10
>>> from 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
2
>>> conn.extend.microsoft.unlock_account(dn)
True

此外该模块下还有 addMembersToGroupsremoveMembersFromGroups 等函数。

参考资料

ldap3 官方文档
ldap3 项目主页