From e1228ad02589e6b8b0c9668a7e1345c35113f44b Mon Sep 17 00:00:00 2001 From: Roland Gruber Date: Thu, 25 Jun 2026 07:26:50 +0200 Subject: [PATCH 01/22] #716 audit log --- lam/lib/config.inc | 40 ++++++++++++++++--- lam/templates/config/mainmanage.php | 60 +++++++++++++++++++++++------ lam/tests/lib/LAMCfgMainTest.php | 2 +- 3 files changed, 84 insertions(+), 18 deletions(-) diff --git a/lam/lib/config.inc b/lam/lib/config.inc index 08b09e2019..fb46822032 100644 --- a/lam/lib/config.inc +++ b/lam/lib/config.inc @@ -2893,6 +2893,12 @@ class LAMCfgMain { /** log destination ("SYSLOG":syslog, "/...":file, "NONE":none, "REMOTE":server:port) */ public string $logDestination = "SYSLOG"; + /** type of audit log */ + public string $auditLogType = 'NONE'; + + /** target of audit log */ + public string $auditLogTarget = ''; + /** list of hosts which may access LAM */ public string $allowedHosts = ""; @@ -3034,6 +3040,7 @@ class LAMCfgMain { /** @var string[] $settings list of data fields to save in config file */ private array $settings = ["password", "default", "sessionTimeout", "hideLoginErrorDetails", "logLevel", "logDestination", "allowedHosts", "passwordMinLength", + 'auditLogType', 'auditLogTarget', "passwordMinUpper", "passwordMinLower", "passwordMinNumeric", "passwordMinClasses", "passwordMinSymbol", 'checkedRulesCount', 'passwordMustNotContainUser', 'passwordMustNotContain3Chars', @@ -3703,11 +3710,34 @@ class LAMCfgMain { * @return bool is valid */ public static function isValidLogFilename(string $path): bool { - return !empty($path) - && preg_match("/^[a-z0-9\\/._-]+$/i", $path) - && (str_ends_with($path, '.log') || str_ends_with($path, '.txt')) - && !str_contains($path, '..') - && !str_starts_with($path, './'); + $realPath = file_exists($path) ? realpath($path) : $path; + $blockedPrefixes = ['/usr', '/etc', '/dev', '/boot', '/lib', '/proc', '/root', '/run', '/sys', '/snap']; + if (!empty($_SERVER['DOCUMENT_ROOT'])) { + $blockedPrefixes[] = $_SERVER['DOCUMENT_ROOT']; + } + foreach ($blockedPrefixes as $blockedPrefix) { + if (str_starts_with($realPath, $blockedPrefix)) { + return false; + } + } + return !empty($realPath) + && preg_match("/^[a-z0-9\\/._-]+$/i", $realPath) + && (str_ends_with($realPath, '.log') || str_ends_with($realPath, '.txt')) + && !str_contains($realPath, '..') + && !str_starts_with($realPath, './'); } } + +enum LOG_FILE_TARGET: string { + + /** audit log entries are sent to file */ + case FILE = 'FILE'; + /** audit log entries are sent to remote server */ + case REMOTE = 'REMOTE'; + /** audit log entries are sent to syslog */ + case SYSLOG = 'SYSLOG'; + /** audit log is disabled */ + case NONE = 'NONE'; + +} diff --git a/lam/templates/config/mainmanage.php b/lam/templates/config/mainmanage.php index 28896c8028..4b78329d9a 100644 --- a/lam/templates/config/mainmanage.php +++ b/lam/templates/config/mainmanage.php @@ -32,6 +32,7 @@ use LAMConfig; use LAMException; use LamTemporaryFilesManager; +use LOG_FILE_TARGET; use PDO; /* @@ -295,18 +296,6 @@ } else { $isValidLogFile = isset($_POST['logFile']) && LAMCfgMain::isValidLogFilename($_POST['logFile']); - $blockedPrefixes = ['/usr', '/etc', '/dev', '/boot', '/lib', '/proc', '/root', '/run', '/sys', '/snap']; - if (!empty($_SERVER['DOCUMENT_ROOT'])) { - $blockedPrefixes[] = $_SERVER['DOCUMENT_ROOT']; - } - foreach ($blockedPrefixes as $blockedPrefix) { - if (!$isValidLogFile) { - break; - } - if (str_starts_with($_POST['logFile'], $blockedPrefix)) { - $isValidLogFile = false; - } - } if ($isValidLogFile) { $cfg->logDestination = $_POST['logFile']; } @@ -314,6 +303,29 @@ $errors[] = _("The log file is empty or contains invalid characters! Valid characters are: a-z, A-Z, 0-9, /, ., _ and -. The file must end with '.log' or '.txt'."); } } + // audit log + if (LOG_FILE_TARGET::tryFrom($_POST['auditLogType']) !== null) { + $cfg->auditLogType = $_POST['auditLogType']; + if ($cfg->auditLogType === LOG_FILE_TARGET::FILE->name) { + $isValidLogFile = isset($_POST['auditLogFile']) && LAMCfgMain::isValidLogFilename($_POST['auditLogFile']); + if ($isValidLogFile) { + $cfg->auditLogTarget = $_POST['auditLogFile']; + } + else { + $errors[] = _("The log file is empty or contains invalid characters! Valid characters are: a-z, A-Z, 0-9, /, ., _ and -. The file must end with '.log' or '.txt'."); + } + } + elseif ($cfg->auditLogType === LOG_FILE_TARGET::REMOTE->name) { + $remoteParts = explode(':', $_POST['auditLogRemote']); + if ((count($remoteParts) !== 2) || !get_preg($remoteParts[0], 'DNSname') || !get_preg($remoteParts[1], 'digit')) { + $errors[] = _("Please enter a valid remote server in format \"server:port\"."); + } + else { + $cfg->auditLogTarget = $_POST['auditLogRemote']; + } + } + } + // password policies $cfg->passwordMinLength = intval($_POST['passwordMinLength']); $cfg->passwordMinLower = intval($_POST['passwordMinLower']); @@ -706,6 +718,30 @@ $errorLogSelect = new htmlResponsiveSelect('errorReporting', $errorLogOptions, [$cfg->errorReporting], _('PHP error reporting'), '244'); $errorLogSelect->setHasDescriptiveElements(true); $row->add($errorLogSelect); + // audit log + $auditDestinationOptions = [ + _("No logging") => LOG_FILE_TARGET::NONE->name, + _("System logging") => LOG_FILE_TARGET::SYSLOG->name, + _("File") => LOG_FILE_TARGET::FILE->name, + _("Remote") => LOG_FILE_TARGET::REMOTE->name, + ]; + $auditLogDestinationSelect = new htmlResponsiveSelect('auditLogType', $auditDestinationOptions, [$cfg->auditLogType], _("Audit log destination"), '240'); + $auditLogDestinationSelect->setTableRowsToHide([ + LOG_FILE_TARGET::NONE->name => ['auditLogFile', 'auditLogRemote'], + LOG_FILE_TARGET::SYSLOG->name => ['auditLogFile', 'auditLogRemote'], + LOG_FILE_TARGET::REMOTE->name => ['auditLogFile'], + LOG_FILE_TARGET::FILE->name => ['auditLogRemote'] + ]); + $auditLogDestinationSelect->setTableRowsToShow([ + LOG_FILE_TARGET::FILE->name => ['auditLogFile'], + LOG_FILE_TARGET::REMOTE->name => ['auditLogRemote'] + ]); + $auditLogDestinationSelect->setHasDescriptiveElements(true); + $row->add($auditLogDestinationSelect); + $auditLogFile = $cfg->auditLogType === LOG_FILE_TARGET::FILE->name ? $cfg->auditLogTarget : ''; + $row->add(new htmlResponsiveInputField(_('File'), 'auditLogFile', $auditLogFile)); + $auditLogRemote = $cfg->auditLogType === LOG_FILE_TARGET::REMOTE->name ? $cfg->auditLogTarget : ''; + $row->add(new htmlResponsiveInputField(_('Remote server'), 'auditLogRemote', $auditLogRemote, '251')); // mail options if (isLAMProVersion()) { diff --git a/lam/tests/lib/LAMCfgMainTest.php b/lam/tests/lib/LAMCfgMainTest.php index 48b984d509..ecfb790c6e 100644 --- a/lam/tests/lib/LAMCfgMainTest.php +++ b/lam/tests/lib/LAMCfgMainTest.php @@ -215,7 +215,7 @@ public function testImportData_invalid() { } public function testModuleSettings() { - $settings = ['abc' => 123]; + $settings = ['posixAccount' => ['key' => '123']]; $this->conf->setModuleSettings($settings); $this->assertEquals($settings, $this->conf->getModuleSettings()); From ae3e08eba8705da390f6c988999336fa142b5127 Mon Sep 17 00:00:00 2001 From: Roland Gruber Date: Thu, 25 Jun 2026 07:39:33 +0200 Subject: [PATCH 02/22] #716 audit log --- lam/lib/security.inc | 68 ++++++++++++++++++++++---------------------- 1 file changed, 34 insertions(+), 34 deletions(-) diff --git a/lam/lib/security.inc b/lam/lib/security.inc index 0c73171859..cab3dc13bc 100644 --- a/lam/lib/security.inc +++ b/lam/lib/security.inc @@ -332,6 +332,40 @@ function logNewMessage($level, $message): void { } } +/** + * Logs a message to a remote logging service. + * + * @param int $level log level + * @param string $message log message + * @param LAMCfgMain $cfgMain main configuration + */ +function lamLogRemoteMessage($level, $message, $cfgMain): void { + include_once __DIR__ . '/3rdParty/composer/autoload.php'; + $remoteParts = explode(':', $cfgMain->logDestination); + $server = $remoteParts[1]; + $port = intval($remoteParts[2]); + $output = "%channel%.%level_name%: %message%"; + $formatter = new Monolog\Formatter\LineFormatter($output); + $logger = new Monolog\Logger('lam'); + $syslogHandler = new Monolog\Handler\SyslogUdpHandler($server, $port); + $syslogHandler->setFormatter($formatter); + $logger->pushHandler($syslogHandler); + switch ($level) { + case LOG_DEBUG: + $logger->debug($message); + break; + case LOG_NOTICE: + $logger->notice($message); + break; + case LOG_WARNING: + $logger->warning($message); + break; + case LOG_ERR: + $logger->error($message); + break; + } +} + /** * Checks if write access to LDAP is allowed. * @@ -1051,40 +1085,6 @@ function lamEncryptionAlgo() { return 'AES256'; } -/** - * Logs a message to a remote logging service. - * - * @param int $level log level - * @param string $message log message - * @param LAMCfgMain $cfgMain main configuration - */ -function lamLogRemoteMessage($level, $message, $cfgMain): void { - include_once __DIR__ . '/3rdParty/composer/autoload.php'; - $remoteParts = explode(':', $cfgMain->logDestination); - $server = $remoteParts[1]; - $port = intval($remoteParts[2]); - $output = "%channel%.%level_name%: %message%"; - $formatter = new Monolog\Formatter\LineFormatter($output); - $logger = new Monolog\Logger('lam'); - $syslogHandler = new Monolog\Handler\SyslogUdpHandler($server, $port); - $syslogHandler->setFormatter($formatter); - $logger->pushHandler($syslogHandler); - switch ($level) { - case LOG_DEBUG: - $logger->debug($message); - break; - case LOG_NOTICE: - $logger->notice($message); - break; - case LOG_WARNING: - $logger->warning($message); - break; - case LOG_ERR: - $logger->error($message); - break; - } -} - /** * Manages temporary files. */ From 8c7ae7dc7255f032da5fee89d194641fb6e162d3 Mon Sep 17 00:00:00 2001 From: Roland Gruber Date: Thu, 25 Jun 2026 07:53:47 +0200 Subject: [PATCH 03/22] #716 audit log --- lam/lib/security.inc | 34 ++++++++++++++++++++++++++++++++++ lam/templates/login.php | 1 + 2 files changed, 35 insertions(+) diff --git a/lam/lib/security.inc b/lam/lib/security.inc index cab3dc13bc..f17583c1e3 100644 --- a/lam/lib/security.inc +++ b/lam/lib/security.inc @@ -366,6 +366,40 @@ function lamLogRemoteMessage($level, $message, $cfgMain): void { } } +function logAuditMessage($message): void { + if (isset($_SESSION['cfgMain'])) { + $cfg = $_SESSION['cfgMain']; + } + else { + $cfg = new LAMCfgMain(); + $_SESSION['cfgMain'] = $cfg; + } + // check if logging is disabled + if ($cfg->auditLogType === LOG_FILE_TARGET::NONE->name) { + return; + } + $message = "LDAP Account Manager AuditLog (" . session_id() . ' - ' . getClientIPForLogging() . ' - ' . getLamLdapUser() . ") - " . $message; + if ($cfg->auditLogType === LOG_FILE_TARGET::SYSLOG->name) { + syslog(LOG_NOTICE, str_replace(["\n", "\r"], ['', ''], $message)); + } + elseif (($cfg->auditLogType === LOG_FILE_TARGET::FILE->name) && LAMCfgMain::isValidLogFilename($cfg->auditLogTarget)) { + @touch($cfg->auditLogTarget); + if (is_writable($cfg->auditLogTarget)) { + $file = fopen($cfg->auditLogTarget, 'a'); + if ($file) { + $timeZone = 'UTC'; + $sysTimeZone = @date_default_timezone_get(); + if (!empty($sysTimeZone)) { + $timeZone = $sysTimeZone; + } + $time = new DateTime('now', new DateTimeZone($timeZone)); + fwrite($file, $time->format('Y-m-d H:i:s') . ' ' . $message . "\n"); + fclose($file); + } + } + } +} + /** * Checks if write access to LDAP is allowed. * diff --git a/lam/templates/login.php b/lam/templates/login.php index 5712aa97d3..17e19fce3f 100644 --- a/lam/templates/login.php +++ b/lam/templates/login.php @@ -615,6 +615,7 @@ function displayLoginHeader(): void { addSecurityTokenToSession(); // logging logNewMessage(LOG_NOTICE, 'User ' . $username . ' (' . $clientSource . ') successfully logged in.'); + logAuditMessage('Login to server profile: ' . $_SESSION['config']->getName()); // Load main frame or 2 factor page if ($_SESSION['config']->getTwoFactorAuthentication() == TwoFactorProviderService::TWO_FACTOR_NONE) { metaRefresh("./main.php"); From 44901a6b0df55ee6634bbe83d2f6e58ac05bfcdd Mon Sep 17 00:00:00 2001 From: Roland Gruber Date: Fri, 26 Jun 2026 07:57:13 +0200 Subject: [PATCH 04/22] #716 audit log --- lam/lib/account.inc | 17 +++++++++++++++++ lam/templates/tools/ou_edit.php | 7 +++---- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/lam/lib/account.inc b/lam/lib/account.inc index 3178dbfc9e..0a4c2bd254 100644 --- a/lam/lib/account.inc +++ b/lam/lib/account.inc @@ -1187,6 +1187,23 @@ function moveDn(string $oldDn, string $targetDn): void { } } +/** + * Adds a new LDAP entry. + * + * @param Connection $connection LDAP connection + * @param string $dn DN + * @param array $entry attributes + * @return bool the operation was successful + */ +function ldapAddNewEntry(Connection $connection, string $dn, array $entry): bool { + logNewMessage(LOG_DEBUG, 'Adding new entry: ' . $dn . ' with attributes: ' . json_encode($entry, JSON_PRETTY_PRINT)); + logAuditMessage('Adding new entry: ' . $dn . ' with attributes: ' . json_encode($entry, JSON_PRETTY_PRINT)); + $result = @ldap_add($connection, $dn, $entry); + logNewMessage(LOG_DEBUG, 'Operation ' . ($result ? 'was successful' : 'failed') . '.'); + logAuditMessage('Operation ' . ($result ? 'was successful' : 'failed') . '.'); + return $result; +} + /** * Returns the parameters for a StatusMessage of the last LDAP search. * diff --git a/lam/templates/tools/ou_edit.php b/lam/templates/tools/ou_edit.php index 06f89baa5e..4950d95c12 100644 --- a/lam/templates/tools/ou_edit.php +++ b/lam/templates/tools/ou_edit.php @@ -137,10 +137,9 @@ function refreshOus(array &$optionsToInsert, array &$optionsToDelete): void { $found = ldapGetDN($new_dn); if ($found === null) { // add new ou - $ou = []; - $ou['objectClass'] = "organizationalunit"; - $ou['ou'] = $_POST['newOU']; - $ret = @ldap_add($_SESSION['ldap']->server(), $new_dn, $ou); + $ou['objectClass'][] = "organizationalunit"; + $ou['ou'][] = $_POST['newOU']; + $ret = ldapAddNewEntry($_SESSION['ldap']->server(), $new_dn, $ou); if ($ret) { $message = _("New OU created successfully."); refreshOus($optionsToInsert, $optionsToDelete); From 95782e9c7b9cdb13a87786e7a9d316bfce2853a7 Mon Sep 17 00:00:00 2001 From: Roland Gruber Date: Mon, 29 Jun 2026 07:57:39 +0200 Subject: [PATCH 05/22] #716 audit log --- lam/lib/account.inc | 4 ++-- lam/lib/import.inc | 2 +- lam/lib/modules.inc | 2 +- lam/lib/modules/asteriskExtension.inc | 2 +- lam/lib/modules/fixed_ip.inc | 2 +- lam/lib/modules/inetOrgPerson.inc | 4 ++-- lam/lib/treeview.inc | 2 +- lam/lib/upload.inc | 2 +- 8 files changed, 10 insertions(+), 10 deletions(-) diff --git a/lam/lib/account.inc b/lam/lib/account.inc index 0a4c2bd254..c1245b7d40 100644 --- a/lam/lib/account.inc +++ b/lam/lib/account.inc @@ -1158,7 +1158,7 @@ function copyDnRecursive(string $oldDn, string $targetDn): void { unset($attributes[$systemAttributeToSkip]); } } - $success = ldap_add($_SESSION['ldap']->server(), $newDn, $attributes); + $success = ldapAddNewEntry($_SESSION['ldap']->server(), $newDn, $attributes); if (!$success) { logNewMessage(LOG_ERR, sprintf(_('Was unable to create DN: %s.'), unescapeLdapSpecialCharacters($oldDn)) . ' ' . getExtendedLDAPErrorMessage($_SESSION['ldap']->server())); @@ -1200,7 +1200,7 @@ function ldapAddNewEntry(Connection $connection, string $dn, array $entry): bool logAuditMessage('Adding new entry: ' . $dn . ' with attributes: ' . json_encode($entry, JSON_PRETTY_PRINT)); $result = @ldap_add($connection, $dn, $entry); logNewMessage(LOG_DEBUG, 'Operation ' . ($result ? 'was successful' : 'failed') . '.'); - logAuditMessage('Operation ' . ($result ? 'was successful' : 'failed') . '.'); + logAuditMessage('Operation ' . ($result ? 'was successful' : 'failed') . '. Server returned ' . ldap_errno($connection) . ' - ' . ldap_error($connection)); return $result; } diff --git a/lam/lib/import.inc b/lam/lib/import.inc index a11b1bf3c1..b478129838 100644 --- a/lam/lib/import.inc +++ b/lam/lib/import.inc @@ -461,7 +461,7 @@ class AddEntryTask implements ImporterTask { */ public function run(): string { $ldap = $_SESSION['ldap']->server(); - $success = @ldap_add($ldap, $this->dn, $this->attributes); + $success = ldapAddNewEntry($ldap, $this->dn, $this->attributes); if ($success) { return Importer::formatMessage('INFO', _('Entry created'), htmlspecialchars($this->dn)); } diff --git a/lam/lib/modules.inc b/lam/lib/modules.inc index 777a60313d..f012a429d8 100644 --- a/lam/lib/modules.inc +++ b/lam/lib/modules.inc @@ -1988,7 +1988,7 @@ class accountContainer { if (isset($attributes[$this->finalDN]['modify']) && is_array($attributes[$this->finalDN]['modify'])) { $attr = array_merge_recursive($attr, $attributes[$this->finalDN]['modify']); } - $success = @ldap_add($_SESSION['ldap']->server(), $this->finalDN, $attr); + $success = ldapAddNewEntry($_SESSION['ldap']->server(), $this->finalDN, $attr); if (!$success) { logNewMessage(LOG_ERR, 'Unable to create DN: ' . $this->finalDN . ' (' . ldap_error($_SESSION['ldap']->server()) . '). ' . getExtendedLDAPErrorMessage($_SESSION['ldap']->server())); diff --git a/lam/lib/modules/asteriskExtension.inc b/lam/lib/modules/asteriskExtension.inc index a64c3233cf..97dd91b9e3 100644 --- a/lam/lib/modules/asteriskExtension.inc +++ b/lam/lib/modules/asteriskExtension.inc @@ -901,7 +901,7 @@ class asteriskExtension extends baseModule { // add new config rows for ($rowCounter = count($this->extensionRowsOrig); $rowCounter < count($this->extensionRows); $rowCounter++) { $row = $this->extensionRows[$rowCounter]; - ldap_add($_SESSION['ldap']->server(), "cn=" . $row["cn"][0] . "," . $this->getAccountContainer()->dnSuffix, $row); + ldapAddNewEntry($_SESSION['ldap']->server(), "cn=" . $row["cn"][0] . "," . $this->getAccountContainer()->dnSuffix, $row); } //a trick for Edit again to work diff --git a/lam/lib/modules/fixed_ip.inc b/lam/lib/modules/fixed_ip.inc index 397c4f19ed..51619a4a96 100644 --- a/lam/lib/modules/fixed_ip.inc +++ b/lam/lib/modules/fixed_ip.inc @@ -692,7 +692,7 @@ class fixed_ip extends baseModule { $attr['dhcpComments'][] = $arr['description']; } if ($attr['cn'] != "") { - ldap_add($_SESSION['ldap']->server(), 'cn=' . $arr['cn'] . $ldapSuffix, $attr); + ldapAddNewEntry($_SESSION['ldap']->server(), 'cn=' . $arr['cn'] . $ldapSuffix, $attr); } } // entries to modify diff --git a/lam/lib/modules/inetOrgPerson.inc b/lam/lib/modules/inetOrgPerson.inc index 698ba8529c..c337ef64de 100644 --- a/lam/lib/modules/inetOrgPerson.inc +++ b/lam/lib/modules/inetOrgPerson.inc @@ -914,7 +914,7 @@ class inetOrgPerson extends baseModule implements passwordService, AccountStatus 'objectClass' => ['organizationalUnit'], 'ou' => 'addressbook' ]; - $success = @ldap_add($_SESSION['ldap']->server(), $dn, $attrs); + $success = ldapAddNewEntry($_SESSION['ldap']->server(), $dn, $attrs); if (!$success) { logNewMessage(LOG_ERR, 'Unable to add addressbook for user ' . $accountContainer->finalDN . ' (' . ldap_error($_SESSION['ldap']->server()) . ').'); $messages[] = ['ERROR', sprintf(_("Was unable to create DN: %s."), htmlspecialchars($dn)), getDefaultLDAPErrorString($_SESSION['ldap']->server())]; @@ -2587,7 +2587,7 @@ class inetOrgPerson extends baseModule implements passwordService, AccountStatus 'ou' => 'addressbook' ]; $dn = 'ou=addressbook,' . $accounts[$temp['counter']]['dn']; - $success = @ldap_add($_SESSION['ldap']->server(), $dn, $attrs); + $success = ldapAddNewEntry($_SESSION['ldap']->server(), $dn, $attrs); if (!$success) { logNewMessage(LOG_ERR, 'Unable to add addressbook for user ' . $accounts[$temp['counter']]['dn'] . ' (' . ldap_error($_SESSION['ldap']->server()) . ').'); $errors[] = ['ERROR', sprintf(_("Was unable to create DN: %s."), htmlspecialchars($dn)), getDefaultLDAPErrorString($_SESSION['ldap']->server())]; diff --git a/lam/lib/treeview.inc b/lam/lib/treeview.inc index 46b38a8783..3cc8285414 100644 --- a/lam/lib/treeview.inc +++ b/lam/lib/treeview.inc @@ -1418,7 +1418,7 @@ class TreeView { } $rdn = $rdnAttribute . '=' . ldap_escape($attributes[$rdnAttribute][0], '', LDAP_ESCAPE_DN); $newDn = $rdn . ',' . $dn; - $success = ldap_add($_SESSION['ldap']->server(), $newDn, $attributes); + $success = ldapAddNewEntry($_SESSION['ldap']->server(), $newDn, $attributes); if (!$success) { return $this->createNewNodeCheckObjectClassesStep($dn, getExtendedLDAPErrorMessage($_SESSION['ldap']->server()), $rdnAttribute, $attributes); } diff --git a/lam/lib/upload.inc b/lam/lib/upload.inc index a2aef2a803..c78e61452e 100644 --- a/lam/lib/upload.inc +++ b/lam/lib/upload.inc @@ -168,7 +168,7 @@ class Uploader { return; } // add LDAP entry - $success = @ldap_add($_SESSION['ldap']->server(), $dn, $attrs); + $success = ldapAddNewEntry($_SESSION['ldap']->server(), $dn, $attrs); if (!$success) { $errorMessage = [ "ERROR", From ebfe0452d690f8080b181ef3e831aee04c15cfdf Mon Sep 17 00:00:00 2001 From: Roland Gruber Date: Mon, 29 Jun 2026 16:35:35 +0200 Subject: [PATCH 06/22] #716 audit log --- lam/lib/modules/fixed_ip.inc | 10 +++++----- lam/lib/modules/inetOrgPerson.inc | 4 ++-- lam/lib/modules/nisMailAliasUser.inc | 2 +- lam/lib/modules/posixAccount.inc | 2 +- lam/lib/modules/range.inc | 2 +- lam/lib/security.inc | 7 ++++++- 6 files changed, 16 insertions(+), 11 deletions(-) diff --git a/lam/lib/modules/fixed_ip.inc b/lam/lib/modules/fixed_ip.inc index 51619a4a96..13c018ac8b 100644 --- a/lam/lib/modules/fixed_ip.inc +++ b/lam/lib/modules/fixed_ip.inc @@ -677,17 +677,17 @@ class fixed_ip extends baseModule { foreach ($add as $arr) { $attr = []; - $attr['cn'] = $arr['cn']; - $attr['objectClass'][0] = 'top'; - $attr['objectClass'][1] = 'dhcpHost'; - $attr['dhcpHWAddress'] = 'ethernet ' . $arr['mac']; + $attr['cn'][] = $arr['cn']; + $attr['objectClass'][] = 'top'; + $attr['objectClass'][] = 'dhcpHost'; + $attr['dhcpHWAddress'][] = 'ethernet ' . $arr['mac']; if ($arr['ip'] != '') { $attr['dhcpStatements'][] = 'fixed-address ' . $arr['ip']; } if ($arr['active'] === false) { $attr['dhcpStatements'][] = 'deny booting'; } - $attr['dhcpOption'] = 'host-name "' . $arr['cn'] . '"'; + $attr['dhcpOption'][] = 'host-name "' . $arr['cn'] . '"'; if (!empty($arr['description'])) { $attr['dhcpComments'][] = $arr['description']; } diff --git a/lam/lib/modules/inetOrgPerson.inc b/lam/lib/modules/inetOrgPerson.inc index c337ef64de..7ec9f57893 100644 --- a/lam/lib/modules/inetOrgPerson.inc +++ b/lam/lib/modules/inetOrgPerson.inc @@ -912,7 +912,7 @@ class inetOrgPerson extends baseModule implements passwordService, AccountStatus if (empty($result)) { $attrs = [ 'objectClass' => ['organizationalUnit'], - 'ou' => 'addressbook' + 'ou' => ['addressbook'] ]; $success = ldapAddNewEntry($_SESSION['ldap']->server(), $dn, $attrs); if (!$success) { @@ -2584,7 +2584,7 @@ class inetOrgPerson extends baseModule implements passwordService, AccountStatus if ($this->isBooleanConfigOptionSet('inetOrgPerson_addAddressbook')) { $attrs = [ 'objectClass' => ['organizationalUnit'], - 'ou' => 'addressbook' + 'ou' => ['addressbook'] ]; $dn = 'ou=addressbook,' . $accounts[$temp['counter']]['dn']; $success = ldapAddNewEntry($_SESSION['ldap']->server(), $dn, $attrs); diff --git a/lam/lib/modules/nisMailAliasUser.inc b/lam/lib/modules/nisMailAliasUser.inc index 7a53fed9a0..8a7a664544 100644 --- a/lam/lib/modules/nisMailAliasUser.inc +++ b/lam/lib/modules/nisMailAliasUser.inc @@ -456,7 +456,7 @@ class nisMailAliasUser extends baseModule { // create new aliases foreach ($this->aliasesToAdd as $dn => $attrs) { unset($attrs['dn']); - $success = @ldap_add($_SESSION['ldap']->server(), $dn, $attrs); + $success = ldapAddNewEntry($_SESSION['ldap']->server(), $dn, $attrs); if (!$success) { logNewMessage(LOG_ERR, 'Unable to create mail alias ' . $dn . ' (' . ldap_error($_SESSION['ldap']->server()) . ').'); $errors[] = ['ERROR', sprintf(_('Was unable to create DN: %s.'), $dn), getDefaultLDAPErrorString($_SESSION['ldap']->server())]; diff --git a/lam/lib/modules/posixAccount.inc b/lam/lib/modules/posixAccount.inc index 667b6d73da..76c8984506 100644 --- a/lam/lib/modules/posixAccount.inc +++ b/lam/lib/modules/posixAccount.inc @@ -673,7 +673,7 @@ class posixAccount extends baseModule implements passwordService, AccountStatusP 'objectClass' => ['posixGroup'], ]; } - $newGroupSuccess = @ldap_add(getLDAPServerHandle(), $dnNewGroup, $attributesNewGroup); + $newGroupSuccess = ldapAddNewEntry(getLDAPServerHandle(), $dnNewGroup, $attributesNewGroup); if ($newGroupSuccess) { logNewMessage(LOG_NOTICE, 'Created new group: ' . $newGroupName); $this->attributes['gidNumber'][0] = (string) $nextGid[0]; diff --git a/lam/lib/modules/range.inc b/lam/lib/modules/range.inc index 66555cb866..88b5d79cb8 100644 --- a/lam/lib/modules/range.inc +++ b/lam/lib/modules/range.inc @@ -603,7 +603,7 @@ class range extends baseModule { if (isset($pool['dn'])) { unset($pool['dn']); } - $success = @ldap_add($_SESSION['ldap']->server(), $dn, $pool); + $success = ldapAddNewEntry($_SESSION['ldap']->server(), $dn, $pool); if (!$success) { $msg = sprintf(_('Was unable to create DN: %s.'), $dn); logNewMessage(LOG_ERR, $msg . getDefaultLDAPErrorString($_SESSION['ldap']->server())); diff --git a/lam/lib/security.inc b/lam/lib/security.inc index f17583c1e3..6030243e48 100644 --- a/lam/lib/security.inc +++ b/lam/lib/security.inc @@ -366,7 +366,12 @@ function lamLogRemoteMessage($level, $message, $cfgMain): void { } } -function logAuditMessage($message): void { +/** + * Logs a new audit message. + * + * @param string $message message + */ +function logAuditMessage(string $message): void { if (isset($_SESSION['cfgMain'])) { $cfg = $_SESSION['cfgMain']; } From 21e24fe8ab5576e7bf69c430719b63204395a33b Mon Sep 17 00:00:00 2001 From: Roland Gruber Date: Mon, 29 Jun 2026 16:40:30 +0200 Subject: [PATCH 07/22] #716 audit log --- lam/lib/modules/posixAccount.inc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lam/lib/modules/posixAccount.inc b/lam/lib/modules/posixAccount.inc index 76c8984506..59a9fc9454 100644 --- a/lam/lib/modules/posixAccount.inc +++ b/lam/lib/modules/posixAccount.inc @@ -669,7 +669,7 @@ class posixAccount extends baseModule implements passwordService, AccountStatusP else { $attributesNewGroup = [ 'cn' => [$newGroupName], - 'gidNumber' => $nextGid[0], + 'gidNumber' => [(string) $nextGid[0]], 'objectClass' => ['posixGroup'], ]; } From 6adb25a25e60dac33883ee3d2f16c7800fa3a9a7 Mon Sep 17 00:00:00 2001 From: Roland Gruber Date: Mon, 29 Jun 2026 16:49:26 +0200 Subject: [PATCH 08/22] #716 audit log --- lam/templates/initsuff.php | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/lam/templates/initsuff.php b/lam/templates/initsuff.php index c431c48310..be4583ed11 100644 --- a/lam/templates/initsuff.php +++ b/lam/templates/initsuff.php @@ -74,20 +74,20 @@ $end = implode(",", $tmp); if ($name[0] !== "ou") { // add root entry $attr = []; - $attr[$name[0]] = $name[1]; - $attr['objectClass'] = 'organization'; + $attr[$name[0]] = [$name[1]]; + $attr['objectClass'] = ['organization']; $dn = $suff; - if (!@ldap_add($_SESSION['ldap']->server(), $dn, $attr)) { + if (!ldapAddNewEntry($_SESSION['ldap']->server(), $dn, $attr)) { $failedDNs[$suff] = ldap_error($_SESSION['ldap']->server()); } } else { // add organizational unit $name = $name[1]; $attr = []; - $attr['objectClass'] = "organizationalunit"; - $attr['ou'] = $name; + $attr['objectClass'] = ["organizationalunit"]; + $attr['ou'] = [$name]; $dn = $suff; - if (!@ldap_add($_SESSION['ldap']->server(), $dn, $attr)) { + if (!ldapAddNewEntry($_SESSION['ldap']->server(), $dn, $attr)) { // check if we have to add parent entries if (ldap_errno($_SESSION['ldap']->server()) === 32) { $dnParts = explode(",", $suff); @@ -115,19 +115,19 @@ $headarray = explode("=", $suffarray[0]); $attr = []; if ($headarray[0] === "ou") { // add ou entry - $attr['objectClass'] = 'organizationalunit'; + $attr['objectClass'] = ['organizationalunit']; $attr['ou'] = $headarray[1]; } else { // add root entry $attr['objectClass'][] = 'organization'; $attr[$headarray[0]] = $headarray[1]; if ($headarray[0] === "dc") { - $attr['o'] = $headarray[1]; + $attr['o'] = [$headarray[1]]; $attr['objectClass'][] = 'dcObject'; } } $dn = $subsuffs[$k]; - if (!@ldap_add($_SESSION['ldap']->server(), $dn, $attr)) { + if (!ldapAddNewEntry($_SESSION['ldap']->server(), $dn, $attr)) { $failedDNs[$suff] = ldap_error($_SESSION['ldap']->server()); break; } From 84989678bc14e4d17257c7e58e5cb977bf5e5d51 Mon Sep 17 00:00:00 2001 From: Roland Gruber Date: Mon, 29 Jun 2026 20:22:12 +0200 Subject: [PATCH 09/22] #716 audit log --- lam/lib/account.inc | 18 +++++++++++++++++- lam/lib/import.inc | 2 +- lam/lib/modules/asteriskExtension.inc | 4 ++-- lam/lib/modules/fixed_ip.inc | 2 +- lam/lib/modules/nisMailAliasUser.inc | 2 +- lam/lib/modules/range.inc | 2 +- lam/templates/tools/ou_edit.php | 2 +- 7 files changed, 24 insertions(+), 8 deletions(-) diff --git a/lam/lib/account.inc b/lam/lib/account.inc index c1245b7d40..25f063eaef 100644 --- a/lam/lib/account.inc +++ b/lam/lib/account.inc @@ -1112,7 +1112,7 @@ function deleteDN($dn, $recursive): array { } } // delete parent DN - $success = @ldap_delete($_SESSION['ldap']->server(), $dn); + $success = ldapDeleteEntry($_SESSION['ldap']->server(), $dn); if (!$success) { logNewMessage(LOG_ERR, 'Unable to delete DN: ' . $dn . ' (' . ldap_error($_SESSION['ldap']->server()) . ').'); $errors[] = ['ERROR', sprintf(_('Was unable to delete DN: %s.'), $dn), getDefaultLDAPErrorString($_SESSION['ldap']->server())]; @@ -1204,6 +1204,22 @@ function ldapAddNewEntry(Connection $connection, string $dn, array $entry): bool return $result; } +/** + * Deletes an LDAP entry. + * + * @param Connection $connection connection + * @param string $dn DN + * @return bool success + */ +function ldapDeleteEntry(Connection $connection, string $dn): bool { + logNewMessage(LOG_DEBUG, 'Deleting entry: ' . $dn); + logAuditMessage('Deleting entry: ' . $dn); + $result = @ldap_delete($connection, $dn); + logNewMessage(LOG_DEBUG, 'Operation ' . ($result ? 'was successful' : 'failed') . '.'); + logAuditMessage('Operation ' . ($result ? 'was successful' : 'failed') . '. Server returned ' . ldap_errno($connection) . ' - ' . ldap_error($connection)); + return $result; +} + /** * Returns the parameters for a StatusMessage of the last LDAP search. * diff --git a/lam/lib/import.inc b/lam/lib/import.inc index b478129838..d5f7994b26 100644 --- a/lam/lib/import.inc +++ b/lam/lib/import.inc @@ -533,7 +533,7 @@ class DeleteEntryTask implements ImporterTask { */ public function run(): string { $ldap = $_SESSION['ldap']->server(); - $success = @ldap_delete($ldap, $this->dn); + $success = ldapDeleteEntry($ldap, $this->dn); if ($success) { return Importer::formatMessage('INFO', sprintf(_('Successfully deleted DN %s'), $this->dn), ''); } diff --git a/lam/lib/modules/asteriskExtension.inc b/lam/lib/modules/asteriskExtension.inc index 97dd91b9e3..1fa7b718f0 100644 --- a/lam/lib/modules/asteriskExtension.inc +++ b/lam/lib/modules/asteriskExtension.inc @@ -895,7 +895,7 @@ class asteriskExtension extends baseModule { $this->moveExtentionToNewSuffix($row); } else { - ldap_delete($_SESSION['ldap']->server(), "cn=" . $rowOrig["cn"][0] . "," . extractDNSuffix($this->getAccountContainer()->dn_orig)); + ldapDeleteEntry($_SESSION['ldap']->server(), "cn=" . $rowOrig["cn"][0] . "," . extractDNSuffix($this->getAccountContainer()->dn_orig)); } } // add new config rows @@ -925,7 +925,7 @@ class asteriskExtension extends baseModule { for ($rowCounter = 0; $rowCounter < count($entries); $rowCounter++) { $rowOrig = $entries[$rowCounter]; if ($rowOrig["astpriority"][0] > 1) { - ldap_delete($_SESSION['ldap']->server(), $rowOrig['dn']); + ldapDeleteEntry($_SESSION['ldap']->server(), $rowOrig['dn']); } } return []; diff --git a/lam/lib/modules/fixed_ip.inc b/lam/lib/modules/fixed_ip.inc index 13c018ac8b..a59034adeb 100644 --- a/lam/lib/modules/fixed_ip.inc +++ b/lam/lib/modules/fixed_ip.inc @@ -672,7 +672,7 @@ class fixed_ip extends baseModule { } foreach ($delete as $cn) { - ldap_delete($_SESSION['ldap']->server(), 'cn=' . $cn . $ldapSuffix); + ldapDeleteEntry($_SESSION['ldap']->server(), 'cn=' . $cn . $ldapSuffix); } foreach ($add as $arr) { diff --git a/lam/lib/modules/nisMailAliasUser.inc b/lam/lib/modules/nisMailAliasUser.inc index 8a7a664544..c1e7c6879c 100644 --- a/lam/lib/modules/nisMailAliasUser.inc +++ b/lam/lib/modules/nisMailAliasUser.inc @@ -433,7 +433,7 @@ class nisMailAliasUser extends baseModule { $errors = []; // delete complete aliases foreach ($this->aliasesToDelete as $dn) { - $success = @ldap_delete($_SESSION['ldap']->server(), $dn); + $success = ldapDeleteEntry($_SESSION['ldap']->server(), $dn); if (!$success) { logNewMessage(LOG_ERR, 'Unable to delete ' . $dn . ' (' . ldap_error($_SESSION['ldap']->server()) . ').'); $errors[] = ['ERROR', sprintf(_('Was unable to delete DN: %s.'), $dn), getDefaultLDAPErrorString($_SESSION['ldap']->server())]; diff --git a/lam/lib/modules/range.inc b/lam/lib/modules/range.inc index 88b5d79cb8..751df32f8c 100644 --- a/lam/lib/modules/range.inc +++ b/lam/lib/modules/range.inc @@ -670,7 +670,7 @@ class range extends baseModule { if (!$found) { // delete pool $dn = 'cn=' . $poolOrig['cn'][0] . ',' . $this->getAccountContainer()->finalDN; - $success = @ldap_delete($_SESSION['ldap']->server(), $dn); + $success = ldapDeleteEntry($_SESSION['ldap']->server(), $dn); if (!$success) { $msg = sprintf(_('Was unable to delete DN: %s.'), $dn); logNewMessage(LOG_ERR, $msg . getDefaultLDAPErrorString($_SESSION['ldap']->server())); diff --git a/lam/templates/tools/ou_edit.php b/lam/templates/tools/ou_edit.php index 4950d95c12..9410016643 100644 --- a/lam/templates/tools/ou_edit.php +++ b/lam/templates/tools/ou_edit.php @@ -159,7 +159,7 @@ function refreshOus(array &$optionsToInsert, array &$optionsToDelete): void { } // delete ou, user was sure elseif (isset($_POST['deleteOU']) && isset($_POST['sure']) && in_array_ignore_case($_POST['deletename'], $validDeletableDns)) { - $ret = ldap_delete($_SESSION['ldap']->server(), $_POST['deletename']); + $ret = ldapDeleteEntry($_SESSION['ldap']->server(), $_POST['deletename']); if ($ret) { $message = _("OU deleted successfully."); refreshOus($optionsToInsert, $optionsToDelete); From 83150e979ba2e41f84d43133980e176888adc8e6 Mon Sep 17 00:00:00 2001 From: Roland Gruber Date: Mon, 29 Jun 2026 20:43:36 +0200 Subject: [PATCH 10/22] #716 audit log --- lam/lib/account.inc | 17 +++++++++++++++++ lam/lib/import.inc | 2 +- lam/lib/modules.inc | 2 +- lam/lib/modules/nisMailAliasUser.inc | 2 +- lam/lib/modules/posixAccount.inc | 6 +++--- lam/lib/treeview.inc | 2 +- 6 files changed, 24 insertions(+), 7 deletions(-) diff --git a/lam/lib/account.inc b/lam/lib/account.inc index 25f063eaef..4aa3a6f494 100644 --- a/lam/lib/account.inc +++ b/lam/lib/account.inc @@ -1220,6 +1220,23 @@ function ldapDeleteEntry(Connection $connection, string $dn): bool { return $result; } +/** + * Modifies an LDAP entry. + * + * @param Connection $connection LDAP connection + * @param string $dn DN + * @param array $attributes attributes + * @return bool the operation was successful + */ +function ldapAddAttributes(Connection $connection, string $dn, array $attributes): bool { + logNewMessage(LOG_DEBUG, 'Adding new attributes to ' . $dn . ': ' . json_encode($attributes, JSON_PRETTY_PRINT)); + logAuditMessage('Adding new attributes to ' . $dn . ': ' . json_encode($attributes, JSON_PRETTY_PRINT)); + $result = @ldap_mod_add($connection, $dn, $attributes); + logNewMessage(LOG_DEBUG, 'Operation ' . ($result ? 'was successful' : 'failed') . '.'); + logAuditMessage('Operation ' . ($result ? 'was successful' : 'failed') . '. Server returned ' . ldap_errno($connection) . ' - ' . ldap_error($connection)); + return $result; +} + /** * Returns the parameters for a StatusMessage of the last LDAP search. * diff --git a/lam/lib/import.inc b/lam/lib/import.inc index d5f7994b26..d9adc1acb7 100644 --- a/lam/lib/import.inc +++ b/lam/lib/import.inc @@ -620,7 +620,7 @@ class AddAttributesTask implements ImporterTask { */ public function run(): string { $ldap = $_SESSION['ldap']->server(); - $success = @ldap_mod_add($ldap, $this->dn, $this->attributes); + $success = ldapAddAttributes($ldap, $this->dn, $this->attributes); if ($success) { return ''; } diff --git a/lam/lib/modules.inc b/lam/lib/modules.inc index f012a429d8..cd5f6406e3 100644 --- a/lam/lib/modules.inc +++ b/lam/lib/modules.inc @@ -2026,7 +2026,7 @@ class accountContainer { } // add attributes if (!empty($attributes[$DNs[$i]]['add']) && !$stopprocessing) { - $success = @ldap_mod_add($_SESSION['ldap']->server(), $DNs[$i], $attributes[$DNs[$i]]['add']); + $success = ldapAddAttributes($_SESSION['ldap']->server(), $DNs[$i], $attributes[$DNs[$i]]['add']); if (!$success) { logNewMessage(LOG_ERR, 'Unable to add attributes to DN: ' . $DNs[$i] . ' (' . ldap_error($_SESSION['ldap']->server()) . '). ' . getExtendedLDAPErrorMessage($_SESSION['ldap']->server())); diff --git a/lam/lib/modules/nisMailAliasUser.inc b/lam/lib/modules/nisMailAliasUser.inc index c1e7c6879c..7a1613b2e5 100644 --- a/lam/lib/modules/nisMailAliasUser.inc +++ b/lam/lib/modules/nisMailAliasUser.inc @@ -467,7 +467,7 @@ class nisMailAliasUser extends baseModule { } // add recipients foreach ($this->recipientsToAdd as $dn => $recipients) { - $success = @ldap_mod_add($_SESSION['ldap']->server(), $dn, ['rfc822mailmember' => $recipients]); + $success = ldapAddAttributes($_SESSION['ldap']->server(), $dn, ['rfc822mailmember' => $recipients]); if (!$success) { logNewMessage(LOG_ERR, 'Unable to add recipients ' . implode(', ', $recipients) . ' to ' . $dn . ' (' . ldap_error($_SESSION['ldap']->server()) . ').'); $errors[] = ['ERROR', sprintf(_('Was unable to add attributes to DN: %s.'), $dn), getDefaultLDAPErrorString($_SESSION['ldap']->server())]; diff --git a/lam/lib/modules/posixAccount.inc b/lam/lib/modules/posixAccount.inc index 59a9fc9454..48e1a979f9 100644 --- a/lam/lib/modules/posixAccount.inc +++ b/lam/lib/modules/posixAccount.inc @@ -921,7 +921,7 @@ class posixAccount extends baseModule implements passwordService, AccountStatusP if (in_array_ignore_case('groupOfUniqueNames', $gons[$toAdd[$i]]['objectclass'])) { $attrName = 'uniquemember'; } - $success = @ldap_mod_add($_SESSION['ldap']->server(), $toAdd[$i], [$attrName => [$accountContainer->finalDN]]); + $success = ldapAddAttributes($_SESSION['ldap']->server(), $toAdd[$i], [$attrName => [$accountContainer->finalDN]]); if (!$success) { logNewMessage(LOG_ERR, 'Unable to add user ' . $accountContainer->finalDN . ' to group: ' . $toAdd[$i] . ' (' . ldap_error($_SESSION['ldap']->server()) . ').'); $messages[] = ['ERROR', sprintf(_('Was unable to add attributes to DN: %s.'), $toAdd[$i]), getDefaultLDAPErrorString($_SESSION['ldap']->server())]; @@ -3061,7 +3061,7 @@ class posixAccount extends baseModule implements passwordService, AccountStatusP $errors = []; if (!empty($memberUid)) { $toAdd = ['memberUID' => $memberUid]; - $success = @ldap_mod_add($_SESSION['ldap']->server(), $dnToUpdate, $toAdd); + $success = ldapAddAttributes($_SESSION['ldap']->server(), $dnToUpdate, $toAdd); if (!$success) { $errors[] = [ "ERROR", @@ -3133,7 +3133,7 @@ class posixAccount extends baseModule implements passwordService, AccountStatusP } $errors = []; if (!empty($gonAttrToAdd)) { - $success = @ldap_mod_add($_SESSION['ldap']->server(), $gonDn, $gonAttrToAdd); + $success = ldapAddAttributes($_SESSION['ldap']->server(), $gonDn, $gonAttrToAdd); if (!$success) { $errors[] = [ "ERROR", diff --git a/lam/lib/treeview.inc b/lam/lib/treeview.inc index 3cc8285414..a005e06933 100644 --- a/lam/lib/treeview.inc +++ b/lam/lib/treeview.inc @@ -1104,7 +1104,7 @@ class TreeView { $newModules = array_delete($attributes['olcmoduleload'], $ldapChanges['olcmoduleload']); unset($ldapChanges['olcmoduleload']); if (!empty($newModules)) { - $isOk = ldap_mod_add($_SESSION['ldap']->server(), $dn, ['olcmoduleload' => $newModules]); + $isOk = ldapAddAttributes($_SESSION['ldap']->server(), $dn, ['olcmoduleload' => $newModules]); if (!$isOk) { logNewMessage(LOG_ERR, 'Changing olcmoduleload at ' . $dn . ' failed: ' . getExtendedLDAPErrorMessage($_SESSION['ldap']->server())); } From 33e7b4e11b9f5212482ef2677407869765726f37 Mon Sep 17 00:00:00 2001 From: Roland Gruber Date: Tue, 30 Jun 2026 07:57:01 +0200 Subject: [PATCH 11/22] #716 audit log --- lam/lib/modules/windowsGroup.inc | 2 +- lam/lib/modules/windowsUser.inc | 4 ++-- lam/templates/delete.php | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lam/lib/modules/windowsGroup.inc b/lam/lib/modules/windowsGroup.inc index 3965a7a88f..7deef9604b 100644 --- a/lam/lib/modules/windowsGroup.inc +++ b/lam/lib/modules/windowsGroup.inc @@ -1020,7 +1020,7 @@ class windowsGroup extends baseModule { // add groups for ($i = 0; $i < count($toAdd); $i++) { if (in_array($toAdd[$i], $groups)) { - $success = @ldap_mod_add($_SESSION['ldap']->server(), $toAdd[$i], ['member' => [$this->getAccountContainer()->finalDN]]); + $success = ldapAddAttributes($_SESSION['ldap']->server(), $toAdd[$i], ['member' => [$this->getAccountContainer()->finalDN]]); if (!$success) { logNewMessage(LOG_ERR, 'Unable to add group ' . $this->getAccountContainer()->finalDN . ' to group: ' . $toAdd[$i] . ' (' . ldap_error($_SESSION['ldap']->server()) . ').'); $messages[] = ['ERROR', sprintf(_('Was unable to add attributes to DN: %s.'), $toAdd[$i]), getDefaultLDAPErrorString($_SESSION['ldap']->server())]; diff --git a/lam/lib/modules/windowsUser.inc b/lam/lib/modules/windowsUser.inc index 4fd957561f..dbdac4bde2 100644 --- a/lam/lib/modules/windowsUser.inc +++ b/lam/lib/modules/windowsUser.inc @@ -2983,7 +2983,7 @@ class windowsUser extends baseModule implements passwordService, AccountStatusPr // add groups for ($i = 0; $i < count($toAdd); $i++) { if (in_array($toAdd[$i], $groups)) { - $success = @ldap_mod_add($_SESSION['ldap']->server(), $toAdd[$i], ['member' => [$this->getAccountContainer()->finalDN]]); + $success = ldapAddAttributes($_SESSION['ldap']->server(), $toAdd[$i], ['member' => [$this->getAccountContainer()->finalDN]]); if (!$success) { logNewMessage(LOG_ERR, 'Unable to add user ' . $this->getAccountContainer()->finalDN . ' to group: ' . $toAdd[$i] . ' (' . ldap_error($_SESSION['ldap']->server()) . ').'); $messages[] = ['ERROR', sprintf(_('Was unable to add attributes to DN: %s.'), $toAdd[$i]), getDefaultLDAPErrorString($_SESSION['ldap']->server())]; @@ -3520,7 +3520,7 @@ class windowsUser extends baseModule implements passwordService, AccountStatusPr $keys = array_keys($temp['members']); $group = $keys[0]; $member = array_pop($temp['members'][$group]); - $success = @ldap_mod_add($_SESSION['ldap']->server(), $group, ['member' => $member]); + $success = ldapAddAttributes($_SESSION['ldap']->server(), $group, ['member' => $member]); $errors = []; if (!$success) { $errors[] = [ diff --git a/lam/templates/delete.php b/lam/templates/delete.php index a81f75b641..6764be3547 100644 --- a/lam/templates/delete.php +++ b/lam/templates/delete.php @@ -275,7 +275,7 @@ } // add attributes if (isset($attributes[$dn]['add']) && !$stopProcessing) { - $success = ldap_mod_add($_SESSION['ldap']->server(), $dn, $attributes[$dn]['add']); + $success = ldapAddAttributes($_SESSION['ldap']->server(), $dn, $attributes[$dn]['add']); if (!$success) { $errors[] = ['ERROR', sprintf(_('Was unable to add attributes to DN: %s.'), $dn), getDefaultLDAPErrorString($_SESSION['ldap']->server())]; $stopProcessing = true; From 4b9222229631611ea418f258832c54fb028ce7fe Mon Sep 17 00:00:00 2001 From: Roland Gruber Date: Tue, 30 Jun 2026 19:21:36 +0200 Subject: [PATCH 12/22] #716 audit log --- lam/lib/account.inc | 19 ++++++++++++++++++- lam/lib/import.inc | 2 +- lam/lib/modules.inc | 2 +- lam/lib/modules/nisMailAliasUser.inc | 2 +- 4 files changed, 21 insertions(+), 4 deletions(-) diff --git a/lam/lib/account.inc b/lam/lib/account.inc index 4aa3a6f494..488eda85b2 100644 --- a/lam/lib/account.inc +++ b/lam/lib/account.inc @@ -1225,7 +1225,7 @@ function ldapDeleteEntry(Connection $connection, string $dn): bool { * * @param Connection $connection LDAP connection * @param string $dn DN - * @param array $attributes attributes + * @param array $attributes attributes to add * @return bool the operation was successful */ function ldapAddAttributes(Connection $connection, string $dn, array $attributes): bool { @@ -1237,6 +1237,23 @@ function ldapAddAttributes(Connection $connection, string $dn, array $attributes return $result; } +/** + * Modifies an LDAP entry. + * + * @param Connection $connection LDAP connection + * @param string $dn DN + * @param array $attributes attributes to remove + * @return bool the operation was successful + */ +function ldapDeleteAttributes(Connection $connection, string $dn, array $attributes): bool { + logNewMessage(LOG_DEBUG, 'Removing attributes from ' . $dn . ': ' . json_encode($attributes, JSON_PRETTY_PRINT)); + logAuditMessage('Removing attributes from ' . $dn . ': ' . json_encode($attributes, JSON_PRETTY_PRINT)); + $result = @ldap_mod_del($connection, $dn, $attributes); + logNewMessage(LOG_DEBUG, 'Operation ' . ($result ? 'was successful' : 'failed') . '.'); + logAuditMessage('Operation ' . ($result ? 'was successful' : 'failed') . '. Server returned ' . ldap_errno($connection) . ' - ' . ldap_error($connection)); + return $result; +} + /** * Returns the parameters for a StatusMessage of the last LDAP search. * diff --git a/lam/lib/import.inc b/lam/lib/import.inc index d9adc1acb7..04d0392bae 100644 --- a/lam/lib/import.inc +++ b/lam/lib/import.inc @@ -679,7 +679,7 @@ class DeleteAttributesTask implements ImporterTask { public function run(): string { $ldap = $_SESSION['ldap']->server(); if (!empty($this->attributes)) { - $success = @ldap_mod_del($ldap, $this->dn, $this->attributes); + $success = ldapDeleteAttributes($ldap, $this->dn, $this->attributes); } else { $success = @ldap_modify($ldap, $this->dn, [$this->attributeName => []]); diff --git a/lam/lib/modules.inc b/lam/lib/modules.inc index cd5f6406e3..4f56111433 100644 --- a/lam/lib/modules.inc +++ b/lam/lib/modules.inc @@ -2039,7 +2039,7 @@ class accountContainer { } // remove attributes if (!empty($attributes[$DNs[$i]]['remove']) && !$stopprocessing) { - $success = @ldap_mod_del($_SESSION['ldap']->server(), $DNs[$i], $attributes[$DNs[$i]]['remove']); + $success = ldapDeleteAttributes($_SESSION['ldap']->server(), $DNs[$i], $attributes[$DNs[$i]]['remove']); if (!$success) { logNewMessage(LOG_ERR, 'Unable to delete attributes from DN: ' . $DNs[$i] . ' (' . ldap_error($_SESSION['ldap']->server()) . '). ' . getExtendedLDAPErrorMessage($_SESSION['ldap']->server())); diff --git a/lam/lib/modules/nisMailAliasUser.inc b/lam/lib/modules/nisMailAliasUser.inc index 7a1613b2e5..c6fdcbc6fe 100644 --- a/lam/lib/modules/nisMailAliasUser.inc +++ b/lam/lib/modules/nisMailAliasUser.inc @@ -444,7 +444,7 @@ class nisMailAliasUser extends baseModule { } // delete recipient entries foreach ($this->recipientsToDelete as $dn => $recipients) { - $success = @ldap_mod_del($_SESSION['ldap']->server(), $dn, ['rfc822mailmember' => $recipients]); + $success = ldapDeleteAttributes($_SESSION['ldap']->server(), $dn, ['rfc822mailmember' => $recipients]); if (!$success) { logNewMessage(LOG_ERR, 'Unable to remove recipients ' . implode(', ', $recipients) . ' from ' . $dn . ' (' . ldap_error($_SESSION['ldap']->server()) . ').'); $errors[] = ['ERROR', sprintf(_('Was unable to remove attributes from DN: %s.'), $dn), getDefaultLDAPErrorString($_SESSION['ldap']->server())]; From 58bc49f28388a0b10cd9ee2c832187753e1a093b Mon Sep 17 00:00:00 2001 From: Roland Gruber Date: Wed, 1 Jul 2026 07:30:00 +0200 Subject: [PATCH 13/22] #716 audit log --- lam/lib/modules/posixAccount.inc | 8 ++++---- lam/lib/modules/windowsGroup.inc | 2 +- lam/lib/modules/windowsUser.inc | 2 +- lam/templates/delete.php | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/lam/lib/modules/posixAccount.inc b/lam/lib/modules/posixAccount.inc index 48e1a979f9..facf4d0e72 100644 --- a/lam/lib/modules/posixAccount.inc +++ b/lam/lib/modules/posixAccount.inc @@ -938,7 +938,7 @@ class posixAccount extends baseModule implements passwordService, AccountStatusP if (in_array_ignore_case('groupOfUniqueNames', $gons[$toRem[$i]]['objectclass'])) { $attrName = 'uniquemember'; } - $success = @ldap_mod_del($_SESSION['ldap']->server(), $toRem[$i], [$attrName => [$accountContainer->dn_orig]]); + $success = ldapDeleteAttributes($_SESSION['ldap']->server(), $toRem[$i], [$attrName => [$accountContainer->dn_orig]]); if (!$success) { logNewMessage(LOG_ERR, 'Unable to delete user ' . $accountContainer->finalDN . ' from group: ' . $toRem[$i] . ' (' . ldap_error($_SESSION['ldap']->server()) . ').'); $messages[] = ['ERROR', sprintf(_('Was unable to remove attributes from DN: %s.'), $toRem[$i]), getDefaultLDAPErrorString($_SESSION['ldap']->server())]; @@ -1079,7 +1079,7 @@ class posixAccount extends baseModule implements passwordService, AccountStatusP $result = searchLDAPByAttribute('sudoUser', $this->attributes['uid'][0], 'sudoRole', ['dn'], ['sudo']); foreach ($result as $attrs) { $dn = $attrs['dn']; - $success = @ldap_mod_del($_SESSION['ldap']->server(), $dn, ['sudoUser' => [$this->attributes['uid'][0]]]); + $success = ldapDeleteAttributes($_SESSION['ldap']->server(), $dn, ['sudoUser' => [$this->attributes['uid'][0]]]); if (!$success) { $return[] = ['ERROR', getDefaultLDAPErrorString($_SESSION['ldap']->server())]; } @@ -4324,7 +4324,7 @@ class posixAccount extends baseModule implements passwordService, AccountStatusP foreach ($groups as $group) { logNewMessage(LOG_DEBUG, 'Removing ' . $attributes['dn'] . ' from ' . $group['dn'] . ' (' . $searchAttr . ')'); $attributesToDelete = [$searchAttr => [$attributes['dn']]]; - $success = @ldap_mod_del($_SESSION['ldap']->server(), $group['dn'], $attributesToDelete); + $success = ldapDeleteAttributes($_SESSION['ldap']->server(), $group['dn'], $attributesToDelete); if (!$success) { $ldapError = getDefaultLDAPErrorString($_SESSION['ldap']->server()); logNewMessage(LOG_ERR, 'Unable to delete attributes of DN: ' . $group['dn'] . ' (' . $ldapError . ').'); @@ -4348,7 +4348,7 @@ class posixAccount extends baseModule implements passwordService, AccountStatusP foreach ($groups as $group) { logNewMessage(LOG_DEBUG, 'Removing ' . $uid . ' from ' . $group['dn'] . ' (' . $searchAttr . ')'); $attributesToDelete = [$searchAttr => [$uid]]; - $success = @ldap_mod_del($_SESSION['ldap']->server(), $group['dn'], $attributesToDelete); + $success = ldapDeleteAttributes($_SESSION['ldap']->server(), $group['dn'], $attributesToDelete); if (!$success) { $ldapError = getDefaultLDAPErrorString($_SESSION['ldap']->server()); logNewMessage(LOG_ERR, 'Unable to delete attributes of DN: ' . $group['dn'] . ' (' . $ldapError . ').'); diff --git a/lam/lib/modules/windowsGroup.inc b/lam/lib/modules/windowsGroup.inc index 7deef9604b..93d528edc3 100644 --- a/lam/lib/modules/windowsGroup.inc +++ b/lam/lib/modules/windowsGroup.inc @@ -1033,7 +1033,7 @@ class windowsGroup extends baseModule { // remove groups for ($i = 0; $i < count($toRem); $i++) { if (in_array($toRem[$i], $groups)) { - $success = @ldap_mod_del($_SESSION['ldap']->server(), $toRem[$i], ['member' => [$this->getAccountContainer()->dn_orig]]); + $success = ldapDeleteAttributes($_SESSION['ldap']->server(), $toRem[$i], ['member' => [$this->getAccountContainer()->dn_orig]]); if (!$success) { logNewMessage(LOG_ERR, 'Unable to delete group ' . $this->getAccountContainer()->finalDN . ' from group: ' . $toRem[$i] . ' (' . ldap_error($_SESSION['ldap']->server()) . ').'); $messages[] = ['ERROR', sprintf(_('Was unable to remove attributes from DN: %s.'), $toRem[$i]), getDefaultLDAPErrorString($_SESSION['ldap']->server())]; diff --git a/lam/lib/modules/windowsUser.inc b/lam/lib/modules/windowsUser.inc index dbdac4bde2..a12cc2dc65 100644 --- a/lam/lib/modules/windowsUser.inc +++ b/lam/lib/modules/windowsUser.inc @@ -2997,7 +2997,7 @@ class windowsUser extends baseModule implements passwordService, AccountStatusPr for ($i = 0; $i < count($toRem); $i++) { if (in_array($toRem[$i], $groups)) { // membership is removed with potentially new DN as Windows updates group automatically on user move - $success = @ldap_mod_del($_SESSION['ldap']->server(), $toRem[$i], ['member' => [$this->getAccountContainer()->finalDN]]); + $success = ldapDeleteAttributes($_SESSION['ldap']->server(), $toRem[$i], ['member' => [$this->getAccountContainer()->finalDN]]); if (!$success) { logNewMessage(LOG_ERR, 'Unable to delete user ' . $this->getAccountContainer()->finalDN . ' from group: ' . $toRem[$i] . ' (' . ldap_error($_SESSION['ldap']->server()) . ').'); $messages[] = ['ERROR', sprintf(_('Was unable to remove attributes from DN: %s.'), $toRem[$i]), getDefaultLDAPErrorString($_SESSION['ldap']->server())]; diff --git a/lam/templates/delete.php b/lam/templates/delete.php index 6764be3547..9840df8c19 100644 --- a/lam/templates/delete.php +++ b/lam/templates/delete.php @@ -284,7 +284,7 @@ } // remove attributes if (isset($attributes[$dn]['remove']) && !$stopProcessing) { - $success = ldap_mod_del($_SESSION['ldap']->server(), $dn, $attributes[$dn]['remove']); + $success = ldapDeleteAttributes($_SESSION['ldap']->server(), $dn, $attributes[$dn]['remove']); if (!$success) { $errors[] = ['ERROR', sprintf(_('Was unable to remove attributes from DN: %s.'), $dn), getDefaultLDAPErrorString($_SESSION['ldap']->server())]; $stopProcessing = true; From 9f6d378a160b30344890537c164909feb6ee5b14 Mon Sep 17 00:00:00 2001 From: Roland Gruber Date: Wed, 1 Jul 2026 07:44:24 +0200 Subject: [PATCH 14/22] #716 audit log --- lam/lib/account.inc | 17 +++++++++++++++++ lam/lib/import.inc | 4 ++-- lam/lib/modules/fixed_ip.inc | 2 +- lam/lib/modules/posixAccount.inc | 4 ++-- lam/lib/modules/posixGroup.inc | 4 ++-- lam/lib/modules/range.inc | 2 +- lam/lib/modules/windowsUser.inc | 2 +- lam/lib/treeview.inc | 2 +- lam/templates/tools/multiEdit.php | 2 +- 9 files changed, 28 insertions(+), 11 deletions(-) diff --git a/lam/lib/account.inc b/lam/lib/account.inc index 488eda85b2..f2b6598042 100644 --- a/lam/lib/account.inc +++ b/lam/lib/account.inc @@ -1254,6 +1254,23 @@ function ldapDeleteAttributes(Connection $connection, string $dn, array $attribu return $result; } +/** + * Modifies an LDAP entry. + * + * @param Connection $connection LDAP connection + * @param string $dn DN + * @param array $attributes attributes to change + * @return bool the operation was successful + */ +function ldapModifyAttributes(Connection $connection, string $dn, array $attributes): bool { + logNewMessage(LOG_DEBUG, 'Replacing attributes in ' . $dn . ': ' . json_encode($attributes, JSON_PRETTY_PRINT)); + logAuditMessage('Replacing attributes in ' . $dn . ': ' . json_encode($attributes, JSON_PRETTY_PRINT)); + $result = @ldap_mod_replace($connection, $dn, $attributes); + logNewMessage(LOG_DEBUG, 'Operation ' . ($result ? 'was successful' : 'failed') . '.'); + logAuditMessage('Operation ' . ($result ? 'was successful' : 'failed') . '. Server returned ' . ldap_errno($connection) . ' - ' . ldap_error($connection)); + return $result; +} + /** * Returns the parameters for a StatusMessage of the last LDAP search. * diff --git a/lam/lib/import.inc b/lam/lib/import.inc index 04d0392bae..b0d8ab459c 100644 --- a/lam/lib/import.inc +++ b/lam/lib/import.inc @@ -682,7 +682,7 @@ class DeleteAttributesTask implements ImporterTask { $success = ldapDeleteAttributes($ldap, $this->dn, $this->attributes); } else { - $success = @ldap_modify($ldap, $this->dn, [$this->attributeName => []]); + $success = ldapModifyAttributes($ldap, $this->dn, [$this->attributeName => []]); } if ($success) { return ''; @@ -747,7 +747,7 @@ class ReplaceAttributesTask implements ImporterTask { */ public function run(): string { $ldap = $_SESSION['ldap']->server(); - $success = @ldap_modify($ldap, $this->dn, $this->attributes); + $success = ldapModifyAttributes($ldap, $this->dn, $this->attributes); if ($success) { return ''; } diff --git a/lam/lib/modules/fixed_ip.inc b/lam/lib/modules/fixed_ip.inc index a59034adeb..a15f369334 100644 --- a/lam/lib/modules/fixed_ip.inc +++ b/lam/lib/modules/fixed_ip.inc @@ -697,7 +697,7 @@ class fixed_ip extends baseModule { } // entries to modify foreach ($mod as $dn => $attrs) { - ldap_modify($_SESSION['ldap']->server(), $dn, $attrs); + ldapModifyAttributes($_SESSION['ldap']->server(), $dn, $attrs); } } return []; diff --git a/lam/lib/modules/posixAccount.inc b/lam/lib/modules/posixAccount.inc index facf4d0e72..39a098e4c9 100644 --- a/lam/lib/modules/posixAccount.inc +++ b/lam/lib/modules/posixAccount.inc @@ -3296,7 +3296,7 @@ class posixAccount extends baseModule implements passwordService, AccountStatusP if (isset($attrs['uidnumber'][0]) && ($attrs['uidnumber'][0] != '')) { $newValue = intval($attrs['uidnumber'][0]) + $count; $ldapHandle = $_SESSION['ldap']->server(); - ldap_modify($ldapHandle, $dn, ['uidnumber' => [$newValue]]); + ldapModifyAttributes($ldapHandle, $dn, ['uidnumber' => [$newValue]]); logNewMessage(LOG_DEBUG, 'Updated Samba ID pool ' . $dn . ' with UID number ' . $newValue . ' and LDAP code ' . ldap_errno($ldapHandle)); if (ldap_errno($ldapHandle) !== 0) { logNewMessage(LOG_NOTICE, 'Updating Samba ID pool ' . $dn . ' with UID number ' . $newValue . ' failed. ' . ldap_error($ldapHandle)); @@ -3329,7 +3329,7 @@ class posixAccount extends baseModule implements passwordService, AccountStatusP if (!empty($attrs['mssfu30maxuidnumber'][0])) { $newValue = intval($attrs['mssfu30maxuidnumber'][0]) + $count; $ldapHandle = $_SESSION['ldap']->server(); - ldap_modify($ldapHandle, $dn, ['mssfu30maxuidnumber' => [$newValue]]); + ldapModifyAttributes($ldapHandle, $dn, ['mssfu30maxuidnumber' => [$newValue]]); logNewMessage(LOG_DEBUG, 'Updated domain info ' . $dn . ' with UID number ' . $newValue . ' and LDAP code ' . ldap_errno($ldapHandle)); if (ldap_errno($ldapHandle) !== 0) { logNewMessage(LOG_NOTICE, 'Updating domain info ' . $dn . ' with UID number ' . $newValue . ' failed. ' . ldap_error($ldapHandle)); diff --git a/lam/lib/modules/posixGroup.inc b/lam/lib/modules/posixGroup.inc index 7b9c365704..e18135d2fc 100644 --- a/lam/lib/modules/posixGroup.inc +++ b/lam/lib/modules/posixGroup.inc @@ -1188,7 +1188,7 @@ class posixGroup extends baseModule implements passwordService { if (isset($attrs['gidnumber'][0]) && ($attrs['gidnumber'][0] != '')) { $newValue = intval($attrs['gidnumber'][0]) + $count; $ldapHandle = $_SESSION['ldap']->server(); - ldap_modify($ldapHandle, $dn, ['gidnumber' => [$newValue]]); + ldapModifyAttributes($ldapHandle, $dn, ['gidnumber' => [$newValue]]); logNewMessage(LOG_DEBUG, 'Updated Samba ID pool ' . $dn . ' with GID number ' . $newValue . ' and LDAP code ' . ldap_errno($ldapHandle)); if (ldap_errno($ldapHandle) !== 0) { logNewMessage(LOG_NOTICE, 'Updating Samba ID pool ' . $dn . ' with GID number ' . $newValue . ' failed. ' . ldap_error($ldapHandle)); @@ -1216,7 +1216,7 @@ class posixGroup extends baseModule implements passwordService { if (isset($attrs['mssfu30maxgidnumber'][0]) && ($attrs['mssfu30maxgidnumber'][0] != '')) { $newValue = intval($attrs['mssfu30maxgidnumber'][0]) + $count; $ldapHandle = $_SESSION['ldap']->server(); - ldap_modify($ldapHandle, $dn, ['mssfu30maxgidnumber' => [$newValue]]); + ldapModifyAttributes($ldapHandle, $dn, ['mssfu30maxgidnumber' => [$newValue]]); logNewMessage(LOG_DEBUG, 'Updated domain info ' . $dn . ' with GID number ' . $newValue . ' and LDAP code ' . ldap_errno($ldapHandle)); if (ldap_errno($ldapHandle) !== 0) { logNewMessage(LOG_NOTICE, 'Updating domain info ' . $dn . ' with GID number ' . $newValue . ' failed. ' . ldap_error($ldapHandle)); diff --git a/lam/lib/modules/range.inc b/lam/lib/modules/range.inc index 751df32f8c..670345ba66 100644 --- a/lam/lib/modules/range.inc +++ b/lam/lib/modules/range.inc @@ -647,7 +647,7 @@ class range extends baseModule { } if (!empty($mod)) { $dn = 'cn=' . $pool['cn'][0] . ',' . $this->getAccountContainer()->finalDN; - $success = @ldap_modify($_SESSION['ldap']->server(), $dn, $mod); + $success = ldapModifyAttributes($_SESSION['ldap']->server(), $dn, $mod); if (!$success) { $msg = sprintf(_('Was unable to modify attributes of DN: %s.'), $dn); logNewMessage(LOG_ERR, $msg . getDefaultLDAPErrorString($_SESSION['ldap']->server())); diff --git a/lam/lib/modules/windowsUser.inc b/lam/lib/modules/windowsUser.inc index a12cc2dc65..aeee36a518 100644 --- a/lam/lib/modules/windowsUser.inc +++ b/lam/lib/modules/windowsUser.inc @@ -3010,7 +3010,7 @@ class windowsUser extends baseModule implements passwordService, AccountStatusPr // force password change if needed if ($this->pwdLastSet !== null) { $attrs = ['pwdLastSet' => [$this->pwdLastSet]]; - $success = @ldap_modify($_SESSION['ldap']->server(), $this->getAccountContainer()->finalDN, $attrs); + $success = ldapModifyAttributes($_SESSION['ldap']->server(), $this->getAccountContainer()->finalDN, $attrs); if (!$success) { logNewMessage(LOG_ERR, 'Unable to change pwdLastSet for ' . $this->getAccountContainer()->finalDN . ' (' . ldap_error($_SESSION['ldap']->server()) . ').'); $messages[] = ['ERROR', sprintf(_('Was unable to modify attributes of DN: %s.'), $this->getAccountContainer()->finalDN), getDefaultLDAPErrorString($_SESSION['ldap']->server())]; diff --git a/lam/lib/treeview.inc b/lam/lib/treeview.inc index a005e06933..cb49fa1a5f 100644 --- a/lam/lib/treeview.inc +++ b/lam/lib/treeview.inc @@ -1074,7 +1074,7 @@ class TreeView { if (!empty($ldapChanges)) { $this->handleSpecialAttributes($dn, $ldapChanges, $attributes, $schemaAttributes); logNewMessage(LOG_DEBUG, 'LDAP changes to DN ' . $dn . ': ' . print_r($ldapChanges, true)); - $saved = ldap_modify($_SESSION['ldap']->server(), $dn, $ldapChanges); + $saved = ldapModifyAttributes($_SESSION['ldap']->server(), $dn, $ldapChanges); if ($saved) { $message = new htmlStatusMessage('INFO', _('All changes were successful.')); } diff --git a/lam/templates/tools/multiEdit.php b/lam/templates/tools/multiEdit.php index c932e561fa..ea9fc72741 100644 --- a/lam/templates/tools/multiEdit.php +++ b/lam/templates/tools/multiEdit.php @@ -521,7 +521,7 @@ function doModify(): array { $changes = $_SESSION['multiEdit_status']['actions'][$dn]; $_SESSION['multiEdit_status']['modContent'] .= htmlspecialchars($dn) . "
"; // run LDAP commands - $success = ldap_modify($_SESSION['ldap']->server(), $dn, $changes); + $success = ldapModifyAttributes($_SESSION['ldap']->server(), $dn, $changes); if (!$success || isset($_REQUEST['multiEdit_error'])) { $msg = new htmlStatusMessage('ERROR', getDefaultLDAPErrorString($_SESSION['ldap']->server())); $_SESSION['multiEdit_status']['modContent'] .= getMessageHTML($msg); From 7c2987764003b40c66886274dea5d0549b82bfb2 Mon Sep 17 00:00:00 2001 From: Roland Gruber Date: Wed, 1 Jul 2026 07:51:02 +0200 Subject: [PATCH 15/22] #716 audit log --- lam/lib/modules.inc | 2 +- lam/lib/modules/asteriskExtension.inc | 4 ++-- lam/lib/modules/nisNetGroupUser.inc | 6 +++--- lam/lib/modules/posixAccount.inc | 2 +- lam/lib/modules/windowsUser.inc | 2 +- lam/templates/delete.php | 2 +- 6 files changed, 9 insertions(+), 9 deletions(-) diff --git a/lam/lib/modules.inc b/lam/lib/modules.inc index 4f56111433..10279087e3 100644 --- a/lam/lib/modules.inc +++ b/lam/lib/modules.inc @@ -2007,7 +2007,7 @@ class accountContainer { logNewMessage(LOG_DEBUG, 'Attribute changes for ' . $DNs[$i] . ":\n" . print_r($attributes[$DNs[$i]], true)); // modify attributes if (!empty($attributes[$DNs[$i]]['modify'])) { - $success = @ldap_mod_replace($_SESSION['ldap']->server(), $DNs[$i], $attributes[$DNs[$i]]['modify']); + $success = ldapModifyAttributes($_SESSION['ldap']->server(), $DNs[$i], $attributes[$DNs[$i]]['modify']); if (!$success) { logNewMessage(LOG_ERR, 'Unable to modify attributes of DN: ' . $DNs[$i] . ' (' . ldap_error($_SESSION['ldap']->server()) . '). ' . getExtendedLDAPErrorMessage($_SESSION['ldap']->server())); diff --git a/lam/lib/modules/asteriskExtension.inc b/lam/lib/modules/asteriskExtension.inc index 1fa7b718f0..7731891724 100644 --- a/lam/lib/modules/asteriskExtension.inc +++ b/lam/lib/modules/asteriskExtension.inc @@ -879,7 +879,7 @@ class asteriskExtension extends baseModule { } if (!empty($ldapChanges)) { if (!isset($ldapChanges['cn'])) { - ldap_mod_replace($_SESSION['ldap']->server(), "cn=" . $row["cn"][0] . "," . $this->getAccountContainer()->dnSuffix, $ldapChanges); + ldapModifyAttributes($_SESSION['ldap']->server(), "cn=" . $row["cn"][0] . "," . $this->getAccountContainer()->dnSuffix, $ldapChanges); } else { $origDN = "cn=" . $rowOrig["cn"][0] . "," . extractDNSuffix($this->getAccountContainer()->dn_orig); @@ -887,7 +887,7 @@ class asteriskExtension extends baseModule { ldap_rename($_SESSION['ldap']->server(), $origDN, $newRDN, extractDNSuffix($this->getAccountContainer()->dn_orig), true); unset($ldapChanges['cn']); if (!empty($ldapChanges)) { - ldap_mod_replace($_SESSION['ldap']->server(), $newRDN . "," . extractDNSuffix($this->getAccountContainer()->dn_orig), $ldapChanges); + ldapModifyAttributes($_SESSION['ldap']->server(), $newRDN . "," . extractDNSuffix($this->getAccountContainer()->dn_orig), $ldapChanges); } } } diff --git a/lam/lib/modules/nisNetGroupUser.inc b/lam/lib/modules/nisNetGroupUser.inc index fb53c575fa..88e7d8ade2 100644 --- a/lam/lib/modules/nisNetGroupUser.inc +++ b/lam/lib/modules/nisNetGroupUser.inc @@ -346,7 +346,7 @@ class nisNetGroupUser extends baseModule { 'nisnetgrouptriple' => $triples ]; logNewMessage(LOG_DEBUG, 'Updating ' . $dn . ' with ' . print_r($attributesNew, true)); - $success = @ldap_mod_replace($_SESSION['ldap']->server(), $dn, $attributesNew); + $success = ldapModifyAttributes($_SESSION['ldap']->server(), $dn, $attributesNew); if (!$success) { logNewMessage(LOG_ERR, 'Unable to modify attributes of DN: ' . $dn . ' (' . ldap_error($_SESSION['ldap']->server()) . ').'); $messages[] = ['ERROR', sprintf(_('Was unable to modify attributes of DN: %s.'), $dn), getDefaultLDAPErrorString($_SESSION['ldap']->server())]; @@ -392,7 +392,7 @@ class nisNetGroupUser extends baseModule { $attributes = [ 'nisnetgrouptriple' => $triples ]; - $success = @ldap_mod_replace($_SESSION['ldap']->server(), $dn, $attributes); + $success = ldapModifyAttributes($_SESSION['ldap']->server(), $dn, $attributes); if (!$success) { logNewMessage(LOG_ERR, 'Unable to modify attributes of DN: ' . $dn . ' (' . ldap_error($_SESSION['ldap']->server()) . ').'); $return[$this->getAccountContainer()->dn_orig]['errors'][] = ['ERROR', sprintf(_('Was unable to modify attributes of DN: %s.'), $dn), getDefaultLDAPErrorString($_SESSION['ldap']->server())]; @@ -554,7 +554,7 @@ class nisNetGroupUser extends baseModule { $attributes = [ 'nisnetgrouptriple' => $triples ]; - $success = @ldap_mod_replace($_SESSION['ldap']->server(), $dn, $attributes); + $success = ldapModifyAttributes($_SESSION['ldap']->server(), $dn, $attributes); if (!$success) { $errors[] = [ "ERROR", diff --git a/lam/lib/modules/posixAccount.inc b/lam/lib/modules/posixAccount.inc index 39a098e4c9..5c62d1d153 100644 --- a/lam/lib/modules/posixAccount.inc +++ b/lam/lib/modules/posixAccount.inc @@ -903,7 +903,7 @@ class posixAccount extends baseModule implements passwordService, AccountStatusP } if ($found) { $attributesToModify = [$searchAttr => $newOwners]; - $success = @ldap_mod_replace($_SESSION['ldap']->server(), $ownerGroups[$i]['dn'], $attributesToModify); + $success = ldapModifyAttributes($_SESSION['ldap']->server(), $ownerGroups[$i]['dn'], $attributesToModify); if (!$success) { $ldapError = getDefaultLDAPErrorString($_SESSION['ldap']->server()); logNewMessage(LOG_ERR, 'Unable to modify attributes of DN: ' . $ownerGroups[$i]['dn'] . ' (' . $ldapError . ').'); diff --git a/lam/lib/modules/windowsUser.inc b/lam/lib/modules/windowsUser.inc index aeee36a518..9781c34b50 100644 --- a/lam/lib/modules/windowsUser.inc +++ b/lam/lib/modules/windowsUser.inc @@ -3543,7 +3543,7 @@ class windowsUser extends baseModule implements passwordService, AccountStatusPr // force password change elseif (count($temp['pwdChange']) > 0) { $dn = array_pop($temp['pwdChange']); - $success = @ldap_mod_replace($_SESSION['ldap']->server(), $dn, ['pwdLastSet' => '0']); + $success = ldapModifyAttributes($_SESSION['ldap']->server(), $dn, ['pwdLastSet' => '0']); $errors = []; if (!$success) { $errors[] = [ diff --git a/lam/templates/delete.php b/lam/templates/delete.php index 9840df8c19..b183673f52 100644 --- a/lam/templates/delete.php +++ b/lam/templates/delete.php @@ -266,7 +266,7 @@ if (!$stopProcessing) { // modify attributes if (isset($attributes[$dn]['modify'])) { - $success = ldap_mod_replace($_SESSION['ldap']->server(), $dn, $attributes[$dn]['modify']); + $success = ldapModifyAttributes($_SESSION['ldap']->server(), $dn, $attributes[$dn]['modify']); if (!$success) { $errors[] = ['ERROR', sprintf(_('Was unable to modify attributes from DN: %s.'), $dn), getDefaultLDAPErrorString($_SESSION['ldap']->server())]; $stopProcessing = true; From 51aa0de9619d1b72edef7a4fa84275d806605981 Mon Sep 17 00:00:00 2001 From: Roland Gruber Date: Wed, 1 Jul 2026 07:54:53 +0200 Subject: [PATCH 16/22] #716 audit log --- lam/lib/modules/posixAccount.inc | 4 ++-- lam/lib/modules/posixGroup.inc | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lam/lib/modules/posixAccount.inc b/lam/lib/modules/posixAccount.inc index 5c62d1d153..4919d6acaa 100644 --- a/lam/lib/modules/posixAccount.inc +++ b/lam/lib/modules/posixAccount.inc @@ -3296,7 +3296,7 @@ class posixAccount extends baseModule implements passwordService, AccountStatusP if (isset($attrs['uidnumber'][0]) && ($attrs['uidnumber'][0] != '')) { $newValue = intval($attrs['uidnumber'][0]) + $count; $ldapHandle = $_SESSION['ldap']->server(); - ldapModifyAttributes($ldapHandle, $dn, ['uidnumber' => [$newValue]]); + ldapModifyAttributes($ldapHandle, $dn, ['uidnumber' => [(string) $newValue]]); logNewMessage(LOG_DEBUG, 'Updated Samba ID pool ' . $dn . ' with UID number ' . $newValue . ' and LDAP code ' . ldap_errno($ldapHandle)); if (ldap_errno($ldapHandle) !== 0) { logNewMessage(LOG_NOTICE, 'Updating Samba ID pool ' . $dn . ' with UID number ' . $newValue . ' failed. ' . ldap_error($ldapHandle)); @@ -3329,7 +3329,7 @@ class posixAccount extends baseModule implements passwordService, AccountStatusP if (!empty($attrs['mssfu30maxuidnumber'][0])) { $newValue = intval($attrs['mssfu30maxuidnumber'][0]) + $count; $ldapHandle = $_SESSION['ldap']->server(); - ldapModifyAttributes($ldapHandle, $dn, ['mssfu30maxuidnumber' => [$newValue]]); + ldapModifyAttributes($ldapHandle, $dn, ['mssfu30maxuidnumber' => [(string) $newValue]]); logNewMessage(LOG_DEBUG, 'Updated domain info ' . $dn . ' with UID number ' . $newValue . ' and LDAP code ' . ldap_errno($ldapHandle)); if (ldap_errno($ldapHandle) !== 0) { logNewMessage(LOG_NOTICE, 'Updating domain info ' . $dn . ' with UID number ' . $newValue . ' failed. ' . ldap_error($ldapHandle)); diff --git a/lam/lib/modules/posixGroup.inc b/lam/lib/modules/posixGroup.inc index e18135d2fc..feca1b4b7e 100644 --- a/lam/lib/modules/posixGroup.inc +++ b/lam/lib/modules/posixGroup.inc @@ -1188,7 +1188,7 @@ class posixGroup extends baseModule implements passwordService { if (isset($attrs['gidnumber'][0]) && ($attrs['gidnumber'][0] != '')) { $newValue = intval($attrs['gidnumber'][0]) + $count; $ldapHandle = $_SESSION['ldap']->server(); - ldapModifyAttributes($ldapHandle, $dn, ['gidnumber' => [$newValue]]); + ldapModifyAttributes($ldapHandle, $dn, ['gidnumber' => [(string) $newValue]]); logNewMessage(LOG_DEBUG, 'Updated Samba ID pool ' . $dn . ' with GID number ' . $newValue . ' and LDAP code ' . ldap_errno($ldapHandle)); if (ldap_errno($ldapHandle) !== 0) { logNewMessage(LOG_NOTICE, 'Updating Samba ID pool ' . $dn . ' with GID number ' . $newValue . ' failed. ' . ldap_error($ldapHandle)); @@ -1216,7 +1216,7 @@ class posixGroup extends baseModule implements passwordService { if (isset($attrs['mssfu30maxgidnumber'][0]) && ($attrs['mssfu30maxgidnumber'][0] != '')) { $newValue = intval($attrs['mssfu30maxgidnumber'][0]) + $count; $ldapHandle = $_SESSION['ldap']->server(); - ldapModifyAttributes($ldapHandle, $dn, ['mssfu30maxgidnumber' => [$newValue]]); + ldapModifyAttributes($ldapHandle, $dn, ['mssfu30maxgidnumber' => [(string) $newValue]]); logNewMessage(LOG_DEBUG, 'Updated domain info ' . $dn . ' with GID number ' . $newValue . ' and LDAP code ' . ldap_errno($ldapHandle)); if (ldap_errno($ldapHandle) !== 0) { logNewMessage(LOG_NOTICE, 'Updating domain info ' . $dn . ' with GID number ' . $newValue . ' failed. ' . ldap_error($ldapHandle)); From 7e3337433161a0f3766b83fedb86449411b2509a Mon Sep 17 00:00:00 2001 From: Roland Gruber Date: Thu, 2 Jul 2026 07:19:38 +0200 Subject: [PATCH 17/22] refactoring --- lam/templates/misc/ajax.php | 3 --- lam/templates/tools/multiEdit.php | 3 --- 2 files changed, 6 deletions(-) diff --git a/lam/templates/misc/ajax.php b/lam/templates/misc/ajax.php index 5240b47f74..c0b877265b 100644 --- a/lam/templates/misc/ajax.php +++ b/lam/templates/misc/ajax.php @@ -532,9 +532,6 @@ private function buildDnSelectionHtml($dnList, $currentDn): string { parseHtml(null, $mainRow, [], false); $out = ob_get_contents(); ob_end_clean(); - if ($out === false) { - return ''; - } return $out; } diff --git a/lam/templates/tools/multiEdit.php b/lam/templates/tools/multiEdit.php index ea9fc72741..fae137f55a 100644 --- a/lam/templates/tools/multiEdit.php +++ b/lam/templates/tools/multiEdit.php @@ -557,8 +557,5 @@ function getMessageHTML(htmlStatusMessage $msg): string { parseHtml(null, $msg, [], true); $content = ob_get_contents(); ob_end_clean(); - if ($content === false) { - return ''; - } return $content; } From 9a38ea4593f6915a4daf33e0b97f653e1cd3652a Mon Sep 17 00:00:00 2001 From: Roland Gruber Date: Thu, 2 Jul 2026 07:30:17 +0200 Subject: [PATCH 18/22] #716 audit log --- lam/lib/account.inc | 17 +++++++++++++++++ lam/lib/modules/windowsUser.inc | 2 +- lam/lib/treeview.inc | 2 +- 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/lam/lib/account.inc b/lam/lib/account.inc index f2b6598042..8cf3113634 100644 --- a/lam/lib/account.inc +++ b/lam/lib/account.inc @@ -1271,6 +1271,23 @@ function ldapModifyAttributes(Connection $connection, string $dn, array $attribu return $result; } +/** + * Modifies an LDAP entry. + * + * @param Connection $connection LDAP connection + * @param string $dn DN + * @param array $attributes attributes to change in batch mode + * @return bool the operation was successful + */ +function ldapModifyAttributesBatch(Connection $connection, string $dn, array $attributes): bool { + logNewMessage(LOG_DEBUG, 'Replacing attributes in ' . $dn . ': ' . json_encode($attributes, JSON_PRETTY_PRINT)); + logAuditMessage('Replacing attributes in ' . $dn . ': ' . json_encode($attributes, JSON_PRETTY_PRINT)); + $result = @ldap_modify_batch($connection, $dn, $attributes, getCommonLdapControls()); + logNewMessage(LOG_DEBUG, 'Operation ' . ($result ? 'was successful' : 'failed') . '.'); + logAuditMessage('Operation ' . ($result ? 'was successful' : 'failed') . '. Server returned ' . ldap_errno($connection) . ' - ' . ldap_error($connection)); + return $result; +} + /** * Returns the parameters for a StatusMessage of the last LDAP search. * diff --git a/lam/lib/modules/windowsUser.inc b/lam/lib/modules/windowsUser.inc index 9781c34b50..57fea9ef51 100644 --- a/lam/lib/modules/windowsUser.inc +++ b/lam/lib/modules/windowsUser.inc @@ -3939,7 +3939,7 @@ class windowsUser extends baseModule implements passwordService, AccountStatusPr 'values' => [$newPasswordVal] ] ]; - @ldap_modify_batch($_SESSION['ldapHandle']->getServer(), $dn, $operation); + ldapModifyAttributesBatch($_SESSION['ldapHandle']->getServer(), $dn, $operation); $returnCode = ldap_errno($_SESSION['ldapHandle']->getServer()); if ($returnCode !== 0) { $outputMessages = htmlspecialchars(getExtendedLDAPErrorMessage($_SESSION['ldapHandle']->getServer())); diff --git a/lam/lib/treeview.inc b/lam/lib/treeview.inc index cb49fa1a5f..0aa5175c2c 100644 --- a/lam/lib/treeview.inc +++ b/lam/lib/treeview.inc @@ -1464,7 +1464,7 @@ class TreeView { 'values' => [$rdn . ',' . $targetDn] ], ]; - $success = ldap_modify_batch(getLDAPServerHandle(), $dn, $changes, getCommonLdapControls()); + $success = ldapModifyAttributesBatch(getLDAPServerHandle(), $dn, $changes); if ($success) { return json_encode([]); } From 937c455e365ec8dec0f8c0e1936563accf2354f0 Mon Sep 17 00:00:00 2001 From: Roland Gruber Date: Thu, 2 Jul 2026 07:49:58 +0200 Subject: [PATCH 19/22] #716 audit log --- lam/templates/login.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lam/templates/login.php b/lam/templates/login.php index 17e19fce3f..b70fdaeb36 100644 --- a/lam/templates/login.php +++ b/lam/templates/login.php @@ -593,6 +593,7 @@ function displayLoginHeader(): void { if (!$searchSuccess) { $error_message = $searchError; $searchLDAP->close(); + logAuditMessage('Login failed for server profile: ' . $_SESSION['config']->getName()); display_LoginPage($licenseValidator, $error_message); exit(); } @@ -600,6 +601,7 @@ function displayLoginHeader(): void { } catch (LAMException $e) { $searchLDAP->close(); + logAuditMessage('Login failed for server profile: ' . $_SESSION['config']->getName()); display_LoginPage($licenseValidator, $e->getTitle(), $e->getMessage()); exit(); } @@ -639,6 +641,7 @@ function displayLoginHeader(): void { if ($cfgMain->isHideLoginErrorDetails()) { $message = null; } + logAuditMessage('Login failed for server profile: ' . $_SESSION['config']->getName()); display_LoginPage($licenseValidator, $e->getTitle(), $message, $extraMessage); exit(); } From c23d59aba535e6c77afedbebf472abf1e42ee4df Mon Sep 17 00:00:00 2001 From: Roland Gruber Date: Thu, 2 Jul 2026 07:57:25 +0200 Subject: [PATCH 20/22] #716 audit log --- lam/lib/account.inc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lam/lib/account.inc b/lam/lib/account.inc index 8cf3113634..def836be98 100644 --- a/lam/lib/account.inc +++ b/lam/lib/account.inc @@ -1276,7 +1276,7 @@ function ldapModifyAttributes(Connection $connection, string $dn, array $attribu * * @param Connection $connection LDAP connection * @param string $dn DN - * @param array $attributes attributes to change in batch mode + * @param array[] $attributes attribute operations to perform in batch mode * @return bool the operation was successful */ function ldapModifyAttributesBatch(Connection $connection, string $dn, array $attributes): bool { From ab968a036037fe5c614e5d259cb09a36ecd52615 Mon Sep 17 00:00:00 2001 From: Roland Gruber Date: Thu, 2 Jul 2026 19:55:56 +0200 Subject: [PATCH 21/22] #716 audit log --- lam/lib/account.inc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lam/lib/account.inc b/lam/lib/account.inc index def836be98..1aca4bb243 100644 --- a/lam/lib/account.inc +++ b/lam/lib/account.inc @@ -1276,7 +1276,7 @@ function ldapModifyAttributes(Connection $connection, string $dn, array $attribu * * @param Connection $connection LDAP connection * @param string $dn DN - * @param array[] $attributes attribute operations to perform in batch mode + * @param array[] $attributes attribute operations to perform in batch mode * @return bool the operation was successful */ function ldapModifyAttributesBatch(Connection $connection, string $dn, array $attributes): bool { From 2e8ca4375bf3a0cbfe650abe64f8a8f73ab1a9b8 Mon Sep 17 00:00:00 2001 From: Roland Gruber Date: Fri, 3 Jul 2026 07:35:37 +0200 Subject: [PATCH 22/22] #716 audit log --- lam/templates/login2Factor.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lam/templates/login2Factor.php b/lam/templates/login2Factor.php index 1f06bcdb9d..a190b97316 100644 --- a/lam/templates/login2Factor.php +++ b/lam/templates/login2Factor.php @@ -132,11 +132,13 @@ if ($e->getPrevious() !== null) { $errorMessage .= ' - ' . $e->getPrevious()->getMessage(); } + logAuditMessage('Login to server profile failed at 2FA: ' . $config->getName() . ' - ' . $user); logNewMessage(LOG_WARNING, '2-factor verification failed: ' . $errorMessage); header("HTTP/1.1 403 Forbidden"); } if ($twoFactorValid) { unset($_SESSION['2factorRequired']); + logAuditMessage('Login to server profile successful at 2FA: ' . $config->getName() . ' - ' . $user); metaRefresh("main.php"); die(); }