GeoNetAgent, LDAPWeb, server-audit, server-connection
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

218 lines
6.4 KiB

<?php
namespace App\Services;
use InvalidArgumentException;
use RuntimeException;
class LdapConnection
{
private const PROTECTED_USERS = [
'administrator', 'guest', 'krbtgt', 'defaultaccount',
];
private const PROTECTED_GROUPS = [
'domain admins', 'enterprise admins', 'schema admins',
'administrators', 'group policy creator owners',
'denied rodc password replication group',
'domain controllers', 'read-only domain controllers',
'cloneable domain controllers', 'protected users',
'dnsadmins', 'cert publishers',
];
/**
* @return resource
*/
public function connect(bool $ssl = false)
{
if (! extension_loaded('ldap')) {
throw new RuntimeException('LDAP PHP extension is not available.');
}
$host = config('ldap.host');
$useSsl = $ssl || config('ldap.use_ssl');
$port = $useSsl ? (int) config('ldap.ssl_port', 636) : (int) config('ldap.port', 389);
$scheme = $useSsl ? 'ldaps' : 'ldap';
if ($useSsl) {
$this->configureTls();
}
$connection = ldap_connect("{$scheme}://{$host}:{$port}");
if ($connection === false) {
throw new RuntimeException('LDAP connect failed.');
}
ldap_set_option($connection, LDAP_OPT_PROTOCOL_VERSION, 3);
ldap_set_option($connection, LDAP_OPT_REFERRALS, 0);
ldap_set_option($connection, LDAP_OPT_NETWORK_TIMEOUT, config('ldap.timeout', 10));
return $connection;
}
/**
* @return resource
*/
public function bind(bool $ssl = false)
{
$bindDn = config('ldap.bind_dn');
$bindPassword = config('ldap.bind_password');
if ($bindDn === '' || $bindPassword === '') {
throw new RuntimeException('LDAP credentials are not configured.');
}
$connection = $this->connect($ssl);
if (! @ldap_bind($connection, $bindDn, $bindPassword)) {
$error = ldap_error($connection);
@ldap_unbind($connection);
throw new RuntimeException('LDAP bind failed.' . ($error ? " ({$error})" : ''));
}
return $connection;
}
private function configureTls(): void
{
if (! config('ldap.tls_verify', false)) {
putenv('LDAPTLS_REQCERT=never');
@ldap_set_option(null, LDAP_OPT_X_TLS_REQUIRE_CERT, LDAP_OPT_X_TLS_NEVER);
}
}
public function findUserDn($connection, string $username): string
{
$username = $this->validateUsername($username);
$filter = '(sAMAccountName=' . ldap_escape($username, '', LDAP_ESCAPE_FILTER) . ')';
$result = @ldap_search($connection, config('ldap.base_dn'), $filter, ['dn']);
if ($result === false) {
throw new RuntimeException('LDAP user search failed.');
}
$entries = ldap_get_entries($connection, $result);
if (($entries['count'] ?? 0) < 1) {
throw new InvalidArgumentException('User not found.');
}
return $entries[0]['dn'];
}
public function findGroupDn($connection, string $groupKey): string
{
$groupKey = trim($groupKey);
$escaped = ldap_escape($groupKey, '', LDAP_ESCAPE_FILTER);
$filter = "(&(|(sAMAccountName={$escaped})(cn={$escaped}))(objectClass=group))";
$result = @ldap_search($connection, config('ldap.base_dn'), $filter, ['dn', 'cn', 'samaccountname']);
if ($result === false) {
throw new RuntimeException('LDAP group search failed.');
}
$entries = ldap_get_entries($connection, $result);
if (($entries['count'] ?? 0) < 1) {
throw new InvalidArgumentException('Group not found.');
}
return $entries[0]['dn'];
}
public function validateUsername(string $username): string
{
$username = trim($username);
if (! preg_match('/^[a-zA-Z0-9._-]+$/', $username)) {
throw new InvalidArgumentException('Invalid username.');
}
return $username;
}
public function assertUserWritable(string $username): void
{
$username = mb_strtolower($this->validateUsername($username));
$protected = array_map('mb_strtolower', config('ldap.protected_users', self::PROTECTED_USERS));
if (in_array($username, $protected, true)) {
throw new InvalidArgumentException('This account is protected and cannot be modified.');
}
}
public function assertGroupWritable(string $groupName): void
{
$groupName = mb_strtolower(trim($groupName));
$protected = array_map('mb_strtolower', config('ldap.protected_groups', self::PROTECTED_GROUPS));
if (in_array($groupName, $protected, true)) {
throw new InvalidArgumentException('This group is protected and cannot be modified.');
}
}
public function domainFromBaseDn(?string $baseDn = null): string
{
$baseDn = $baseDn ?? config('ldap.base_dn');
preg_match_all('/DC=([^,]+)/i', $baseDn, $matches);
return implode('.', $matches[1] ?? []);
}
public function attributeValue(array $entry, string $key): ?string
{
$key = strtolower($key);
if (! isset($entry[$key]) || ! is_array($entry[$key])) {
return null;
}
return $entry[$key][0] ?? null;
}
public function membersList(array $entry): array
{
if (! isset($entry['member']) || ! is_array($entry['member'])) {
return [];
}
$members = [];
for ($i = 0; $i < ($entry['member']['count'] ?? 0); $i++) {
$dn = $entry['member'][$i] ?? '';
if (preg_match('/^CN=([^,]+)/i', $dn, $matches)) {
$members[] = $matches[1];
}
}
sort($members);
return $members;
}
public function memberUsernames($connection, array $memberDns): array
{
$usernames = [];
foreach ($memberDns as $dn) {
$result = @ldap_read($connection, $dn, '(objectClass=*)', ['samaccountname']);
if ($result === false) {
continue;
}
$entries = ldap_get_entries($connection, $result);
$name = $this->attributeValue($entries[0] ?? [], 'samaccountname');
if ($name !== null) {
$usernames[] = $name;
}
}
sort($usernames);
return $usernames;
}
}