Ldap.php
4.79 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
<?php
final class LdapException extends RuntimeException
{
}
final class Ldap
{
/** @var resource|\LDAP\Connection */
private $connect;
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);
// O AD recusa operações sensíveis (troca de senha via unicodePwd) em canal
// não criptografado ("Server is unwilling to perform"). Se o servidor não
// for ldaps://, negocia STARTTLS antes de qualquer bind.
if (!str_starts_with(strtolower($server), 'ldaps://')) {
if (!@ldap_start_tls($this->connect)) {
throw new LdapException(
'Falha ao negociar STARTTLS com o LDAP: ' . ldap_error($this->connect) .
'. Troque ldap.server para ldaps:// no config/local.php se o servidor não suportar STARTTLS.'
);
}
}
}
/**
* 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
{
$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 . '"');
}
}