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.
 
 
 
 
 
 

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