Files
ADconsle/app/Services/OuTreeService.php
T
kzbkowskiandClaude Sonnet 5 1126d96271
CI / PHP 8.2 (push) Failing after 2m46s
CI / PHP 8.3 (push) Failing after 59s
CI / PHP 8.4 (push) Failing after 1m7s
ADconsole v1.0.0
First public release.

- docs: document the audit log, user offboarding, CSV export and the
  "require a reason for destructive actions" setting across README, docs/
  and CLAUDE/AGENTS.md; note retention for panel_audit
- docs: fill the publishing placeholders (repo URL, security contact,
  issue-template links) and add a semantic-versioning policy
- move agent/contributor context into CLAUDE/ (AGENTS.md + a README);
  root CLAUDE.md is now a thin @-import pointer
- add a favicon (shield-lock, matching the login screen) to the login
  page and the app layout
- CHANGELOG: promote [Unreleased] to [1.0.0]; keep the earlier internal
  build as [0.1.0]

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UfuvfwexztdS7LDpGuKsuK
2026-09-08 23:14:39 +02:00

200 lines
6.2 KiB
PHP

<?php
namespace App\Services;
use Illuminate\Support\Facades\Cache;
use LdapRecord\Models\ActiveDirectory\Computer;
use LdapRecord\Models\ActiveDirectory\Group;
use LdapRecord\Models\ActiveDirectory\OrganizationalUnit;
use LdapRecord\Models\ActiveDirectory\User;
/**
* Buduje drzewo OU ograniczone do zdefiniowanych rootów domeny,
* np. LDAP_DEFAULT_USER_OU oraz LDAP_DEFAULT_COMPUTER_OU.
*
* Zbudowane drzewo (wraz z licznikami obiektów) jest kosztowne - dla każdego
* OU leci rekursywne zapytanie zliczające - więc trzymamy je w cache pliku
* na TTL sekund. Przycisk "odśwież" w sidebarze woła OuTreeService::flush().
*/
class OuTreeService
{
/** Scope'y, dla których budujemy drzewo. */
public const SCOPES = ['users', 'computers', 'groups', 'gpo'];
protected const CACHE_TTL = 300;
protected array $tree = [];
/**
* Czyści cache drzewa OU dla wszystkich modułów.
*/
public static function flush(): void
{
foreach (self::SCOPES as $scope) {
Cache::store('file')->forget("ou_tree:{$scope}");
}
}
/**
* DN najwyższego węzła (rootu) drzewa dla danego scope - używane do
* domyślnego zaznaczenia OU po wejściu na stronę.
*/
public function rootDn(string $scope = 'users'): ?string
{
$tree = $this->tree($scope);
return $tree[0]['dn'] ?? null;
}
/**
* Zwraca drzewo OU dla konkretnego modułu (users, computers, groups, gpo).
*/
public function tree(string $scope = 'users'): array
{
$scopeKey = strtolower($scope);
if (isset($this->tree[$scopeKey])) {
return $this->tree[$scopeKey];
}
// json_encode/decode spłaszcza referencje wewnątrz drzewa (children są
// dopinane przez &$node) - bez tego cache serializowałby dziwne grafy.
$build = fn () => json_decode(json_encode($this->build($scopeKey)), true);
$tree = (self::CACHE_TTL > 0 && ! app()->runningUnitTests())
? Cache::store('file')->remember("ou_tree:{$scopeKey}", self::CACHE_TTL, $build)
: $build();
return $this->tree[$scopeKey] = $tree;
}
/**
* Właściwe zbudowanie drzewa z LDAP (bez cache).
*/
protected function build(string $scopeKey): array
{
$allowedRoots = $this->allowedRootsForScope($scopeKey);
$ous = OrganizationalUnit::query()
->select(['ou', 'distinguishedname'])
->get();
$byDn = [];
foreach ($ous as $ou) {
$dn = trim((string) $ou->getDn());
if (! $this->isWithinAllowedRoots($dn, $allowedRoots)) {
continue;
}
$normalizedDn = strtolower($dn);
$byDn[$normalizedDn] = [
'dn' => $dn,
'name' => $ou->getFirstAttribute('ou') ?? $dn,
'children' => [],
'count' => 0,
];
}
$roots = [];
foreach ($byDn as $normalizedDn => &$node) {
$parentDn = $this->parentDn($normalizedDn);
while ($parentDn && ! isset($byDn[$parentDn])) {
$parentDn = $this->parentDn($parentDn);
}
if ($parentDn && isset($byDn[$parentDn])) {
$byDn[$parentDn]['children'][] = &$node;
} else {
$roots[] = &$node;
}
}
unset($node);
foreach ($roots as &$root) {
$this->attachCounts($root, $scopeKey);
}
unset($root);
return $roots;
}
protected function attachCounts(array &$node, string $scope): int
{
foreach ($node['children'] as &$child) {
$this->attachCounts($child, $scope);
}
unset($child);
// countObjectsInOu() liczy już rekursywnie (całe poddrzewo danego OU),
// więc NIE wolno dodawać sum z dzieci - to powodowało wielokrotne
// zliczanie tych samych obiektów (np. root 313 zamiast 121).
$node['count'] = $this->countObjectsInOu($node['dn'], $scope);
return $node['count'];
}
protected function countObjectsInOu(string $ouDn, string $scope): int
{
$query = match ($scope) {
'users' => User::query()->in($ouDn)->recursive(),
'computers' => Computer::query()->in($ouDn)->recursive(),
'groups' => Group::query()->in($ouDn)->recursive(),
default => null,
};
if (! $query) {
return 0;
}
return (int) $query->select(['distinguishedname'])->get()->count();
}
protected function allowedRootsForScope(string $scope): array
{
$userOu = trim((string) config('ldap.defaults.user_ou', ''));
$computerOu = trim((string) config('ldap.defaults.computer_ou', ''));
$groupsOu = trim((string) config('ldap.defaults.groups_ou', ''));
$baseDn = trim((string) config('ldap.connections.default.base_dn', ''));
return match ($scope) {
'users' => array_values(array_filter([$userOu], fn ($value) => $value !== '')),
'computers' => array_values(array_filter([$computerOu], fn ($value) => $value !== '')),
'groups' => array_values(array_filter([$groupsOu ?: $userOu], fn ($value) => $value !== '')),
'gpo' => array_values(array_filter([$baseDn], fn ($value) => $value !== '')),
default => array_values(array_filter([$userOu, $computerOu, $groupsOu], fn ($value) => $value !== '')),
};
}
protected function isWithinAllowedRoots(string $dn, array $allowedRoots): bool
{
$normalizedDn = strtolower(trim($dn));
foreach ($allowedRoots as $root) {
$normalizedRoot = strtolower(trim((string) $root));
if ($normalizedRoot === '') {
continue;
}
if ($normalizedDn === $normalizedRoot || str_ends_with($normalizedDn, ','.$normalizedRoot)) {
return true;
}
}
return false;
}
protected function parentDn(string $dn): ?string
{
$parts = explode(',', $dn);
array_shift($parts);
$parent = implode(',', $parts);
return $parent === '' ? null : strtolower($parent);
}
}