ldap->bind(); $result = @ldap_search( $connection, config('ldap.base_dn'), self::GROUP_FILTER, ['cn', 'description', 'member', 'grouptype', 'samaccountname', 'distinguishedname'] ); if ($result === false) { return $this->errorResult('LDAP group search failed.'); } $entries = ldap_get_entries($connection, $result); $rows = []; for ($i = 0; $i < ($entries['count'] ?? 0); $i++) { $row = $this->mapGroupEntry($entries[$i]); if ($row !== null) { $rows[] = $row; } } if ($search !== '') { $needle = mb_strtolower($search); $rows = array_values(array_filter($rows, function (array $row) use ($needle) { return str_contains(mb_strtolower($row['name']), $needle) || str_contains(mb_strtolower($row['sam_account_name']), $needle) || str_contains(mb_strtolower($row['description']), $needle); })); } usort($rows, function (array $a, array $b) use ($sort, $dir) { if ($sort === 'members_count') { $cmp = $a['members_count'] <=> $b['members_count']; } else { $cmp = ($a[$sort] ?? '') <=> ($b[$sort] ?? ''); } return $dir === 'desc' ? -$cmp : $cmp; }); return [ 'rows' => $rows, 'meta' => [ 'total' => count($rows), 'server' => config('ldap.host'), 'domain' => $this->ldap->domainFromBaseDn(), ], 'error' => null, ]; } catch (Throwable) { return $this->errorResult('Unable to load LDAP groups.'); } finally { if ($connection !== null) { @ldap_unbind($connection); } } } public function getGroup(string $groupKey): array { $connection = null; try { $connection = $this->ldap->bind(); $dn = $this->ldap->findGroupDn($connection, $groupKey); $result = @ldap_read( $connection, $dn, '(objectClass=group)', ['cn', 'description', 'member', 'grouptype', 'samaccountname', 'distinguishedname'] ); if ($result === false) { throw new InvalidArgumentException('Group not found.'); } $entries = ldap_get_entries($connection, $result); $group = $this->mapGroupEntry($entries[0] ?? [], true); if ($group === null) { throw new InvalidArgumentException('Group not found.'); } $memberDns = []; $entry = $entries[0]; for ($i = 0; $i < ($entry['member']['count'] ?? 0); $i++) { $memberDns[] = $entry['member'][$i]; } $group['member_usernames'] = $this->ldap->memberUsernames($connection, $memberDns); $group['protected'] = $this->isProtectedGroup($group['name']); return $group; } finally { if ($connection !== null) { @ldap_unbind($connection); } } } public function createGroup(string $name, ?string $description = null): void { $name = trim($name); if (! preg_match('/^[a-zA-Z0-9 _.-]{1,64}$/', $name)) { throw new InvalidArgumentException('Invalid group name.'); } $this->ldap->assertGroupWritable($name); $connection = null; try { $connection = $this->ldap->bind(); $sam = $this->samFromName($name); $cn = $name; $parentDn = config('ldap.group_container_dn', config('ldap.base_dn')); $dn = "CN={$cn},{$parentDn}"; $entry = [ 'objectClass' => ['top', 'group'], 'cn' => $cn, 'sAMAccountName' => $sam, 'groupType' => (string) self::SECURITY_GROUP_TYPE, ]; if ($description !== null && $description !== '') { $entry['description'] = $description; } if (! @ldap_add($connection, $dn, $entry)) { throw new RuntimeException('Failed to create group. Name may already exist.'); } } finally { if ($connection !== null) { @ldap_unbind($connection); } } } public function deleteGroup(string $groupKey): void { $group = $this->getGroup($groupKey); $this->ldap->assertGroupWritable($group['name']); $connection = null; try { $connection = $this->ldap->bind(); $dn = $this->ldap->findGroupDn($connection, $groupKey); if (! @ldap_delete($connection, $dn)) { throw new RuntimeException('Failed to delete group.'); } } finally { if ($connection !== null) { @ldap_unbind($connection); } } } public function addMember(string $groupKey, string $username): void { $group = $this->getGroup($groupKey); $this->ldap->assertGroupWritable($group['name']); $this->ldap->assertUserWritable($username); $connection = null; try { $connection = $this->ldap->bind(); $groupDn = $this->ldap->findGroupDn($connection, $groupKey); $userDn = $this->ldap->findUserDn($connection, $username); if (! @ldap_mod_add($connection, $groupDn, ['member' => $userDn])) { throw new RuntimeException('Failed to add member. User may already be in the group.'); } } finally { if ($connection !== null) { @ldap_unbind($connection); } } } public function removeMember(string $groupKey, string $username): void { $group = $this->getGroup($groupKey); $this->ldap->assertGroupWritable($group['name']); $connection = null; try { $connection = $this->ldap->bind(); $groupDn = $this->ldap->findGroupDn($connection, $groupKey); $userDn = $this->ldap->findUserDn($connection, $username); if (! @ldap_mod_del($connection, $groupDn, ['member' => $userDn])) { throw new RuntimeException('Failed to remove member.'); } } finally { if ($connection !== null) { @ldap_unbind($connection); } } } public function listUsernamesForSelect(): array { $connection = null; try { $connection = $this->ldap->bind(); $result = @ldap_search( $connection, config('ldap.base_dn'), '(&(objectCategory=person)(objectClass=user))', ['samaccountname'] ); if ($result === false) { return []; } $entries = ldap_get_entries($connection, $result); $names = []; for ($i = 0; $i < ($entries['count'] ?? 0); $i++) { $name = $this->ldap->attributeValue($entries[$i], 'samaccountname'); if ($name !== null) { $names[] = $name; } } sort($names); return $names; } finally { if ($connection !== null) { @ldap_unbind($connection); } } } public function isProtectedGroup(string $name): bool { try { $this->ldap->assertGroupWritable($name); return false; } catch (InvalidArgumentException) { return true; } } public function groupRouteKey(array $group): string { return rawurlencode($group['sam_account_name'] !== '—' ? $group['sam_account_name'] : $group['name']); } private function mapGroupEntry(array $entry, bool $includeMembers = false): ?array { $name = $this->ldap->attributeValue($entry, 'cn'); if ($name === null) { return null; } $groupType = (int) ($this->ldap->attributeValue($entry, 'grouptype') ?? 0); $members = $this->ldap->membersList($entry); $sam = $this->ldap->attributeValue($entry, 'samaccountname') ?? $name; return [ 'name' => $name, 'sam_account_name' => $sam, 'description' => $this->ldap->attributeValue($entry, 'description') ?? '—', 'type' => ($groupType & 0x80000000) ? 'Security' : 'Distribution', 'members' => $members, 'members_count' => count($members), 'protected' => $this->isProtectedGroup($name), 'route_key' => rawurlencode($sam), ]; } private function samFromName(string $name): string { $sam = preg_replace('/[^a-zA-Z0-9._-]/', '', str_replace(' ', '', $name)) ?? ''; if ($sam === '') { throw new InvalidArgumentException('Group name cannot be converted to sAMAccountName.'); } return mb_substr($sam, 0, 20); } private function errorResult(string $message): array { return [ 'rows' => [], 'meta' => [ 'total' => 0, 'server' => config('ldap.host'), 'domain' => $this->ldap->domainFromBaseDn(), ], 'error' => $message, ]; } }