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
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; |
|
} |
|
}
|
|
|