Ldap.php 4.16 KB
<?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);
    }

    /**
     * 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', '@cefetes.br');
        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', '@cefetes.br');
        $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 . '"');
    }
}