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.
334 lines
10 KiB
334 lines
10 KiB
<?php |
|
|
|
namespace App\Services; |
|
|
|
use InvalidArgumentException; |
|
use RuntimeException; |
|
use Throwable; |
|
|
|
class LdapGroupService |
|
{ |
|
private const GROUP_FILTER = '(&(objectCategory=group)(objectClass=group))'; |
|
|
|
private const SECURITY_GROUP_TYPE = -2147483646; |
|
|
|
public function __construct( |
|
private readonly LdapConnection $ldap |
|
) { |
|
} |
|
|
|
public function listGroups(string $sort, string $dir, string $search = ''): array |
|
{ |
|
$connection = null; |
|
|
|
try { |
|
$connection = $this->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, |
|
]; |
|
} |
|
}
|
|
|