[], 'departments' => [], 'roles' => [], ]; } $tenantIds = $this->userTenantRepository->listTenantIdsByUserId($userId); $departmentIds = $this->userDepartmentRepository->listDepartmentIdsByUserId($userId); $roleIds = $this->userRoleRepository->listRoleIdsByUserId($userId); $tenantsById = []; foreach ($this->directoryGateway->listTenantsByIds($tenantIds) as $tenant) { $tenantId = (int) ($tenant['id'] ?? 0); if ($tenantId <= 0) { continue; } $tenantsById[$tenantId] = [ 'id' => $tenantId, 'uuid' => (string) ($tenant['uuid'] ?? ''), 'description' => (string) ($tenant['description'] ?? ''), 'status' => (string) ($tenant['status'] ?? ''), ]; } $departmentsById = []; foreach ($this->directoryGateway->listDepartmentsByIds($departmentIds, true) as $department) { $departmentId = (int) ($department['id'] ?? 0); if ($departmentId <= 0) { continue; } $departmentTenantId = (int) ($department['tenant_id'] ?? 0); $departmentsById[$departmentId] = [ 'id' => $departmentId, 'uuid' => (string) ($department['uuid'] ?? ''), 'description' => (string) ($department['description'] ?? ''), 'active' => (bool) ($department['active'] ?? 0), 'tenant_id' => $departmentTenantId, 'tenant_uuid' => (string) (($tenantsById[$departmentTenantId]['uuid'] ?? '')), ]; } $rolesById = []; foreach ($this->directoryGateway->listRolesByIds($roleIds) as $role) { $roleId = (int) ($role['id'] ?? 0); if ($roleId <= 0) { continue; } $rolesById[$roleId] = [ 'id' => $roleId, 'uuid' => (string) ($role['uuid'] ?? ''), 'description' => (string) ($role['description'] ?? ''), 'active' => (bool) ($role['active'] ?? 0), ]; } $tenants = []; foreach ($tenantIds as $tenantId) { if (isset($tenantsById[$tenantId])) { $tenants[] = $tenantsById[$tenantId]; } } $departments = []; foreach ($departmentIds as $departmentId) { if (isset($departmentsById[$departmentId])) { $departments[] = $departmentsById[$departmentId]; } } $roles = []; foreach ($roleIds as $roleId) { if (isset($rolesById[$roleId])) { $roles[] = $rolesById[$roleId]; } } return [ 'tenants' => $tenants, 'departments' => $departments, 'roles' => $roles, ]; } public function findTenantSummaryInAssignments(array $assignments, int $tenantId): ?array { if ($tenantId <= 0) { return null; } foreach (($assignments['tenants'] ?? []) as $tenant) { $currentTenantId = (int) ($tenant['id'] ?? 0); if ($currentTenantId !== $tenantId) { continue; } return [ 'uuid' => (string) ($tenant['uuid'] ?? ''), 'description' => (string) ($tenant['description'] ?? ''), 'status' => (string) ($tenant['status'] ?? ''), ]; } return null; } public function mapAssignmentsToPublic(array $assignments): array { return [ 'tenants' => array_values(array_map( static fn (array $tenant): array => [ 'uuid' => (string) ($tenant['uuid'] ?? ''), 'description' => (string) ($tenant['description'] ?? ''), 'status' => (string) ($tenant['status'] ?? ''), ], $assignments['tenants'] ?? [] )), 'departments' => array_values(array_map( static fn (array $department): array => [ 'uuid' => (string) ($department['uuid'] ?? ''), 'description' => (string) ($department['description'] ?? ''), 'active' => (bool) ($department['active'] ?? 0), 'tenant_uuid' => (string) ($department['tenant_uuid'] ?? ''), ], $assignments['departments'] ?? [] )), 'roles' => array_values(array_map( static fn (array $role): array => [ 'uuid' => (string) ($role['uuid'] ?? ''), 'description' => (string) ($role['description'] ?? ''), 'active' => (bool) ($role['active'] ?? 0), ], $assignments['roles'] ?? [] )), ]; } public function syncTenants(int $userId, array $tenantIds, bool $bumpAuthz = true): bool { $ids = $this->normalizeTenantIds($tenantIds); $result = $this->userTenantRepository->replaceForUser($userId, $ids); if ($result && $bumpAuthz) { $this->bumpAuthzVersion($userId); } return $result; } public function syncRoles(int $userId, array $roleIds, bool $bumpAuthz = true): bool { $ids = array_values(array_unique(array_map('intval', $roleIds))); $ids = array_values(array_filter($ids, static fn ($id) => $id > 0)); $validIds = $this->directoryGateway->listActiveRoleIds(); if ($validIds) { $validMap = array_fill_keys($validIds, true); $ids = array_values(array_filter($ids, static fn ($id) => isset($validMap[$id]))); } $result = $this->userRoleRepository->replaceForUser($userId, $ids); $this->permissionGateway->clearUserCache($userId); if ($result && $bumpAuthz) { $this->bumpAuthzVersion($userId); } return $result; } public function syncDepartments(int $userId, array $departmentIds, bool $bumpAuthz = true): bool { $ids = array_values(array_unique(array_map('intval', $departmentIds))); $ids = array_values(array_filter($ids, static fn ($id) => $id > 0)); // Important: use the user's assigned tenants directly (no permission bypass), // otherwise privileged users (e.g. admins) would incorrectly allow departments // from tenants that were just removed from their assignments. $userTenantIds = array_values(array_unique(array_map( 'intval', $this->userTenantRepository->listTenantIdsByUserId($userId) ))); $userTenantIds = array_values(array_filter($userTenantIds, static fn ($id) => $id > 0)); if (!$userTenantIds) { $ids = []; } else { $allowedIds = $this->directoryGateway->listActiveDepartmentIdsByTenantIds($userTenantIds); if ($allowedIds) { $allowedMap = array_fill_keys($allowedIds, true); $ids = array_values(array_filter($ids, static fn ($id) => isset($allowedMap[$id]))); } else { $ids = []; } } $result = $this->userDepartmentRepository->replaceForUser($userId, $ids); if ($result && $bumpAuthz) { $this->bumpAuthzVersion($userId); } return $result; } // Incrementing authz_version causes the session to be re-checked on the user's next request. public function bumpAuthzVersion(int $userId): void { if ($userId <= 0) { return; } $this->userWriteRepository->bumpAuthzVersion($userId); } public function syncAllAssignments(int $userId, array $tenantIds, array $roleIds, array $departmentIds): bool { if ($userId <= 0) { return false; } $transactionStarted = false; try { $this->databaseSessionRepository->beginTransaction(); $transactionStarted = true; // bumpAuthz=false on each sync — bump once at the end to avoid redundant DB writes. $ok = $this->syncTenants($userId, $tenantIds, false) && $this->syncRoles($userId, $roleIds, false) && $this->syncDepartments($userId, $departmentIds, false); if (!$ok) { $this->rollbackQuietly(); return false; } $this->bumpAuthzVersion($userId); $this->databaseSessionRepository->commitTransaction(); $transactionStarted = false; return true; } catch (\Throwable $exception) { if ($transactionStarted) { $this->rollbackQuietly(); } return false; } } // Accepts arrays, comma-separated strings, and nested arrays — normalizes to a flat list of IDs. public function normalizeIdInput($value): array { $raw = is_array($value) ? $value : [$value]; $flat = []; $collect = static function ($item) use (&$collect, &$flat): void { if (is_array($item)) { foreach ($item as $nested) { $collect($nested); } return; } $text = trim((string) $item); if ($text === '') { return; } if (str_contains($text, ',')) { foreach (explode(',', $text) as $part) { $part = trim($part); if ($part !== '') { $flat[] = $part; } } return; } $flat[] = $text; }; foreach ($raw as $item) { $collect($item); } $ids = array_values(array_unique(array_map('intval', $flat))); return array_values(array_filter($ids, static fn ($id) => $id > 0)); } public function normalizeTenantIds(array $tenantIds): array { $ids = array_values(array_unique(array_map('intval', $tenantIds))); $ids = array_filter($ids, static fn ($id) => $id > 0); $validIds = $this->directoryGateway->listTenantIds(); if ($validIds) { $validMap = array_fill_keys($validIds, true); $ids = array_values(array_filter($ids, static fn ($id) => isset($validMap[$id]))); } return $ids; } private function rollbackQuietly(): void { try { $this->databaseSessionRepository->rollbackTransaction(); } catch (\Throwable $exception) { // no-op } } }