Ldap.php
5.25 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
<?php
final class LdapException extends RuntimeException
{
}
final class Ldap
{
/** @var resource|\LDAP\Connection */
private $connect;
private bool $encrypted;
public function __construct()
{
$server = Config::get('ldap.server');
$this->connect = ldap_connect($server);
if (!$this->connect) {
throw new LdapException('Não foi possível conectar ao servidor LDAP.');
}
ldap_set_option($this->connect, LDAP_OPT_PROTOCOL_VERSION, 3);
ldap_set_option($this->connect, LDAP_OPT_REFERRALS, 0);
ldap_set_option($this->connect, LDAP_OPT_NETWORK_TIMEOUT, 5);
$this->encrypted = str_starts_with(strtolower($server), 'ldaps://');
// O AD recusa operações sensíveis (troca de senha via unicodePwd) em canal
// não criptografado. Tenta STARTTLS só como bônus: se o servidor não suportar
// (nem todo AD aceita na 389), segue sem criptografia -- buscas/bind continuam
// funcionando normalmente, só a troca de senha vai exigir isso explicitamente.
if (!$this->encrypted && @ldap_start_tls($this->connect)) {
$this->encrypted = true;
}
}
public function isEncrypted(): bool
{
return $this->encrypted;
}
/**
* Tenta autenticar o usuário com a própria senha dele (bind direto).
* Retorna true/false, nunca lança por credencial errada.
*/
public function tryUserBind(string $login, string $password): bool
{
if ($login === '' || $password === '') {
return false;
}
$userPrincipal = $login . Config::get('ldap.domain_suffix', '@example.org');
set_error_handler(static function () {
return true;
}); // silencia warning nativo do ldap_bind em credencial inválida, sem usar @
try {
return (bool) ldap_bind($this->connect, $userPrincipal, $password);
} finally {
restore_error_handler();
}
}
/**
* Faz bind com a conta de serviço (somente leitura), usada para todas as buscas.
*/
public function bindService(): void
{
$user = Config::get('ldap.service_user');
$pass = Config::get('ldap.service_pass');
$userPrincipal = $user . Config::get('ldap.domain_suffix', '@example.org');
$ok = @ldap_bind($this->connect, $userPrincipal, $pass);
if (!$ok) {
throw new LdapException('Falha ao autenticar a conta de serviço no LDAP.');
}
}
public function search(string $filter): array
{
$baseDn = Config::get('ldap.base_dn');
$result = @ldap_search($this->connect, $baseDn, $filter);
if ($result === false) {
throw new LdapException('Erro na busca LDAP: ' . ldap_error($this->connect));
}
$entries = @ldap_get_entries($this->connect, $result);
if ($entries === false) {
throw new LdapException('Erro ao ler entradas LDAP: ' . ldap_error($this->connect));
}
return $entries;
}
public function findUserByCn(string $cn): ?array
{
$filter = '(cn=' . ldap_escape($cn, '', LDAP_ESCAPE_FILTER) . ')';
$entries = $this->search($filter);
return $entries['count'] > 0 ? $entries[0] : null;
}
public function findUserByLogin(string $login): ?array
{
$filter = '(sAMAccountName=' . ldap_escape($login, '', LDAP_ESCAPE_FILTER) . ')';
$entries = $this->search($filter);
return $entries['count'] > 0 ? $entries[0] : null;
}
public function groupMembers(string $groupDnOrFilter): array
{
// aceita tanto um filtro pronto ("cn=grpX") quanto um CN cru
$filter = str_contains($groupDnOrFilter, '=')
? $groupDnOrFilter
: '(cn=' . ldap_escape($groupDnOrFilter, '', LDAP_ESCAPE_FILTER) . ')';
$entries = $this->search($filter);
if ($entries['count'] === 0) {
return [];
}
$members = [];
$count = (int) ($entries[0]['member']['count'] ?? 0);
for ($i = 0; $i < $count; $i++) {
$dn = $entries[0]['member'][$i];
$parts = explode(',', $dn);
$cn = str_replace('CN=', '', $parts[0]);
$members[] = $cn;
}
return $members;
}
public function modify(string $dn, array $attributes): void
{
if (array_key_exists('unicodePwd', $attributes) && !$this->encrypted) {
throw new LdapException(
'Não foi possível negociar um canal seguro (STARTTLS) com o LDAP, e o Active Directory ' .
'exige isso para trocar senha. Peça ao time de AD para liberar STARTTLS na porta 389, ' .
'ou troque ldap.server para ldaps://... (porta 636) no config/local.php.'
);
}
$ok = @ldap_mod_replace($this->connect, $dn, $attributes);
if (!$ok) {
throw new LdapException('Erro ao modificar LDAP: ' . ldap_error($this->connect));
}
}
public function dnOf(array $entry): ?string
{
return $entry['dn'] ?? null;
}
public static function encodePassword(string $plain): string
{
// AD exige a senha entre aspas e em UTF-16LE para unicodePwd.
return iconv('UTF-8', 'UTF-16LE', '"' . $plain . '"');
}
}