From 13d83dbf7521bcb97d489e2abddf61ff16b824f8 Mon Sep 17 00:00:00 2001 From: Benjamin Renard Date: Tue, 21 Mar 2023 10:37:13 +0100 Subject: [PATCH] LSaddon accesslog: global improvments and add self logging feature --- .../LSaddons/config.LSaddons.accesslog.php | 11 + src/css/default/showObjectAccessLogs.css | 36 +++ src/includes/addons/LSaddons.accesslog.php | 271 ++++++++++++++---- .../default/showObjectAccessLogs.tpl | 37 +-- 4 files changed, 286 insertions(+), 69 deletions(-) create mode 100644 src/css/default/showObjectAccessLogs.css diff --git a/src/conf/LSaddons/config.LSaddons.accesslog.php b/src/conf/LSaddons/config.LSaddons.accesslog.php index 502c4cf8..8813f18a 100644 --- a/src/conf/LSaddons/config.LSaddons.accesslog.php +++ b/src/conf/LSaddons/config.LSaddons.accesslog.php @@ -23,3 +23,14 @@ // Accesslog base DN define('LS_ACCESSLOG_BASEDN', 'cn=ldapsaisie-accesslog'); +/* + * Enable logging write events on LDAP entries managed by LdapSaisie in the accesslog base + * + * The feature permit to LdapSaisie to write it self log entries in accesslog base about + * modifications made with its system account with the right author DN. + * + * Note: If your LDAP directory ACL permit to your users to do them self modifications on LDAP + * directory, the recommanded way is to use authz proxy authentication (see useAuthzProxyControl + * configuration parameter). + */ +define('LS_ACCESSLOG_LOG_WRITE_EVENTS', false); diff --git a/src/css/default/showObjectAccessLogs.css b/src/css/default/showObjectAccessLogs.css new file mode 100644 index 00000000..ddb9a25a --- /dev/null +++ b/src/css/default/showObjectAccessLogs.css @@ -0,0 +1,36 @@ +table.objectAccessLogs tbody tr { + border-bottom: 1px dotted; +} + +table.objectAccessLogs table.mods { + width: 100%; + table-layout: fixed; + border-collapse: collapse; +} + +table.objectAccessLogs table.mods td, table.objectAccessLogs table.mods th { + border-left: 1px dotted; +} + +table.objectAccessLogs table.mods tr { + border: none!important; +} + +table.objectAccessLogs .col-attr { + width: 15em; +} + +table.objectAccessLogs .col-op { + width: 6em; +} + +table.objectAccessLogs .col-value { + width: 25em; +} + +table.objectAccessLogs .col-value div { + overflow: scroll; + width: 100%; + margin: 0; + padding: 0; +} diff --git a/src/includes/addons/LSaddons.accesslog.php b/src/includes/addons/LSaddons.accesslog.php index 11b45a3a..7cc7c717 100644 --- a/src/includes/addons/LSaddons.accesslog.php +++ b/src/includes/addons/LSaddons.accesslog.php @@ -25,29 +25,36 @@ LSerror :: defineError('ACCESSLOG_SUPPORT_01', ); $GLOBALS['accesslog_reqTypes'] = array( - 'add' => _('Add'), - 'bind' => _('Log in'), - 'compare' => _('Compare'), - 'delete' => _('Delete'), - 'extended' => _('Extended'), - 'modify' => _('Modify'), - 'modrdn' => _('Modify RDN'), - 'search' => _('Search'), - 'unbind' => _('Log out'), + 'add' => ___('Add'), + 'bind' => ___('Log in'), + 'compare' => ___('Compare'), + 'delete' => ___('Delete'), + 'extended' => ___('Extended'), + 'modify' => ___('Modify'), + 'modrdn' => ___('Modify RDN'), + 'search' => ___('Search'), + 'unbind' => ___('Log out'), ); $GLOBALS['accesslog_modOps'] = array( - '+' => _('Add'), - '-' => _('Delete'), - '=' => _('Replace'), - '' => _('Replace'), - '#' => _('Increment'), + '+' => ___('Add'), + '-' => ___('Delete'), + '=' => ___('Replace'), + '' => ___('Replace'), + '#' => ___('Increment'), ); function LSaddon_accesslog_support() { - if (!defined('LS_ACCESSLOG_BASEDN')) { - LSerror :: addErrorCode('ACCESSLOG_SUPPORT_01', 'LS_ACCESSLOG_BASEDN'); - return false; + $MUST_DEFINE_CONST= array( + 'LS_ACCESSLOG_BASEDN', + 'LS_ACCESSLOG_LOG_WRITE_EVENTS', + ); + + foreach($MUST_DEFINE_CONST as $const) { + if (!defined($const) || is_empty(constant($const))) { + LSerror :: addErrorCode('ACCESSLOG_SUPPORT_01', $const); + return false; + } } if (php_sapi_name() === 'cli') { LScli::add_command( @@ -57,10 +64,18 @@ function LSaddon_accesslog_support() { '[entry DN] [page]' ); } + elseif (LS_ACCESSLOG_LOG_WRITE_EVENTS && LSsession :: loadLSclass('LSldap')) { + LSldap :: addEvent('updated', 'onEntryUpdated'); + LSldap :: addEvent('moved', 'onEntryMoved'); + LSldap :: addEvent('user_password_updated', 'onEntryUserPasswordUpdated'); + LSldap :: addEvent('deleted', 'onEntryDeleted'); + } return true; } function mapAccessLogEntry(&$entry) { + foreach($entry['attrs'] as $attr => $values) + $entry['attrs'][$attr] = ensureIsArray($values); $attrs = $entry['attrs']; $entry['start'] = LSldap::parseDate(LSldap::getAttr($attrs, 'reqStart')); $entry['end'] = LSldap::parseDate(LSldap::getAttr($attrs, 'reqEnd')); @@ -73,49 +88,67 @@ function mapAccessLogEntry(&$entry) { $entry['type'] = LSldap::getAttr($attrs, 'reqType'); $entry['result'] = ldap_err2str(LSldap::getAttr($attrs, 'reqResult')); $entry['message'] = LSldap::getAttr($attrs, 'reqMessage'); - if ($entry['type'] === 'modify' && LSldap::getAttr($attrs, 'reqMod', true)) { - $mods = array(); - foreach(LSldap::getAttr($attrs, 'reqMod', true) as $mod) { - if (preg_match('/^([^\:]+)\:([^ ]?) (.*)$/', $mod, $m)) { - $attr = $m[1]; - $op = $m[2]; - $value = $m[3]; - if (!array_key_exists($attr, $mods)) { - $mods[$attr] = array( - 'mods' => array(), - 'old_values' => array(), - ); - } - $mods[$attr]['changes'][] = array( - 'op' => array_key_exists($op, $GLOBALS['accesslog_modOps']) ? $GLOBALS['accesslog_modOps'][$op] : $op, - 'value' => $value, + $mods = array(); + foreach(LSldap::getAttr($attrs, 'reqMod', true) as $mod) { + if (preg_match('/^(?P[^\:]+)\:(?P[^ ]?)( (?P.*))?$/', $mod, $m)) { + $attr = $m['attr']; + $op = $m['op']; + $value = isset($m['value'])?$m['value']:null; + if (!array_key_exists($attr, $mods)) { + $mods[$attr] = array( + 'changes' => array(), + 'old_values' => array(), ); } + $mods[$attr]['changes'][] = array( + 'op' => ( + array_key_exists($op, $GLOBALS['accesslog_modOps'])? + _($GLOBALS['accesslog_modOps'][$op]): $op + ), + 'value' => $value, + ); } - if (LSldap::getAttr($attrs, 'reqOld', true)) { - foreach(LSldap::getAttr($attrs, 'reqOld', true) as $old) { - if (preg_match('/^([^\:]+)\: (.*)$/', $old, $m) && array_key_exists($m[1], $mods)) { - $mods[$m[1]]['old_values'][] = $m[2]; - } + } + if (LSldap::getAttr($attrs, 'reqOld', true)) { + foreach(LSldap::getAttr($attrs, 'reqOld', true) as $old) { + if (preg_match('/^([^\:]+)\: (.*)$/', $old, $m) && array_key_exists($m[1], $mods)) { + $mods[$m[1]]['old_values'][] = $m[2]; } } + } + if ($mods) $entry['mods'] = $mods; + if ($entry['type'] === 'modrdn') { + $new_rdn = LSldap::getAttr($attrs, 'reqNewRDN', false); + $superior_dn = LSldap::getAttr($attrs, 'reqNewSuperior', false); + if (!$superior_dn) { + $superior_dn = parentDn(LSldap::getAttr($attrs, 'reqDN', false)); + } + $entry['new_dn'] = "$new_rdn,$superior_dn"; } if (array_key_exists($entry['type'], $GLOBALS['accesslog_reqTypes'])) { - $entry['type'] = $GLOBALS['accesslog_reqTypes'][$entry['type']]; + $entry['type'] = _($GLOBALS['accesslog_reqTypes'][$entry['type']]); } } -function sortLogEntryByDate($a, $b) { - return ($a['start'] === $b['start']) ? 0 : ($a['start'] < $b['start']) ? -1 : 1; +function sortLogEntriesByDate(&$a, &$b) { + $astart = LSldap::getAttr($a['attrs'], 'reqStart'); + $bstart = LSldap::getAttr($b['attrs'], 'reqStart'); + return ($astart === $bstart) ? 0 : ($astart < $bstart) ? -1 : 1; } -function getEntryAccessLog($dn) { - $data = LSldap::search( - Net_LDAP2_Filter::create('reqDn', 'equals', $dn), +function getEntryAccessLog($dn, $start_date=null) { + $filter = Net_LDAP2_Filter::create('reqDn', 'equals', $dn); + if ($start_date) { + $date_filter = Net_LDAP2_Filter::create('reqStart', 'greaterOrEqual', $start_date); + $filter = Net_LDAP2_Filter::combine('and', array($filter, $date_filter)); + } + $entries = LSldap::search( + $filter, LS_ACCESSLOG_BASEDN, array( 'attributes' => array( + 'reqDN', 'reqStart', 'reqEnd', 'reqAuthzID', @@ -124,22 +157,33 @@ function getEntryAccessLog($dn) { 'reqMessage', 'reqMod', 'reqOld', + 'reqNewRDN', + 'reqNewSuperior', ), ) ); - if (!is_array($data)) { + if (!is_array($entries)) { return; } + usort($entries, 'sortLogEntriesByDate'); $logs = array(); - foreach($data as $entry) { - foreach($entry['attrs'] as $attr => $values) { - $entry['attrs'][$attr] = ensureIsArray($values); - } + $new_dn = null; + $rename_date = null; + foreach($entries as $entry) { mapAccessLogEntry($entry); $logs[] = $entry; + if (isset($entry['new_dn'])) { + $new_dn = $entry['new_dn']; + $rename_date = LSldap::formatDate($entry['start']); + break; + } } - usort($logs, 'sortLogEntryByDate'); - return array_reverse($logs); + if ($new_dn) { + $next_logs = getEntryAccessLog($new_dn, $rename_date); + if (is_array($next_logs)) + $logs = array_merge($logs, $next_logs); + } + return $start_date?$logs:array_reverse($logs); } function getEntryAccessLogPage($dn, $page = false, $nbByPage = 30) { @@ -183,11 +227,136 @@ function showObjectAccessLogs($obj) { 'action' => 'view', ); LStemplate::assign('LSview_actions', $LSview_actions); + LStemplate::addCSSFile('showObjectAccessLogs.css'); LSsession::setTemplate('showObjectAccessLogs.tpl'); LSsession::displayTemplate(); exit(); } +function onEntryUpdated($data) { + $now = LSldap::formatDate(); + $dn = "reqStart=$now,".LS_ACCESSLOG_BASEDN; + $new_entry = $data['original_entry']->isNew(); + $attrs = array( + 'reqStart' => array($now), + 'reqEnd' => array($now), + 'reqType' => array($new_entry?"add":"modify"), + 'reqSession' => array("1024"), + 'reqAuthzID' => array(LSsession :: get('authenticated_user_dn')), + 'reqDN' => array($data["dn"]), + 'reqResult' => array("0"), + ); + + // Compute modifications + $mods = array(); + $olds = array(); + if ($new_entry) + foreach(ensureIsArray($data['entry']->getValue('objectClass', 'all')) as $value) + $mods[] = "objectClass:+ $value"; + foreach($data['changes'] as $attr => $values) { + if (strtolower($attr) == 'userpassword') + foreach(array_keys($values) as $idx) + $values[$idx] = hashPasswordForLogs($values[$idx]); + if ($values) { + foreach($values as $value) + $mods[] = $new_entry?"$attr:+ $value":"$attr:= $value"; + } + else if (!$new_entry) { + $mods[] = "$attr:="; + } + if (!$new_entry) + foreach(ensureIsArray($data['original_entry']->getValue($attr, 'all')) as $value) + $olds[] = "$attr: $value"; + } + + if (!$mods) return true; + $attrs['reqMod'] = $mods; + if ($olds) + $attrs['reqOld'] = $olds; + + LSldap::getNewEntry($dn, array($new_entry?'auditAdd':'auditModify'), $attrs, true); +} + +function onEntryUserPasswordUpdated($data) { + $now = LSldap::formatDate(); + $dn = "reqStart=$now,".LS_ACCESSLOG_BASEDN; $mods = array(); + + + // Compute modifications + $mods = array(); + + // Retreive fresh entry to retreive hased/stored password + $attrs = LSldap :: getAttrs($data['dn'], null, array('userPassword')); + $new_passwords = $data["new_passwords"]; + if ($attrs) + $new_passwords = LSldap :: getAttr($attrs, 'userPassword', true); + if ($new_passwords) { + foreach($new_passwords as $password) + $mods[] = "userPassword:= ".hashPasswordForLogs($password); + } + else + $mods[] = "userPassword:="; + $attrs = array( + 'reqStart' => array($now), + 'reqEnd' => array($now), + 'reqType' => array("modify"), + 'reqSession' => array("1024"), + 'reqAuthzID' => array(LSsession :: get('authenticated_user_dn')), + 'reqDN' => array($data["dn"]), + 'reqResult' => array("0"), + 'reqMod' => $mods, + ); + LSldap::getNewEntry($dn, array('auditModify'), $attrs, true); +} + +function onEntryMoved($data) { + $now = LSldap::formatDate(); + $dn = "reqStart=$now,".LS_ACCESSLOG_BASEDN; + $attrs = array( + 'reqStart' => array($now), + 'reqEnd' => array($now), + 'reqType' => array("modrdn"), + 'reqSession' => array("1024"), + 'reqAuthzID' => array(LSsession :: get('authenticated_user_dn')), + 'reqDN' => array($data["old"]), + 'reqNewRDN' => array(getRdn($data["new"])), + 'reqResult' => array("0"), + ); + $new_superior = parentDn($data["new"]); + $old_superior = parentDn($data["old"]); + if ($new_superior != $old_superior) + $attrs['reqNewSuperior'] = array($new_superior); + LSldap::getNewEntry($dn, array('auditModRDN'), $attrs, true); +} + +function onEntryDeleted($data) { + $now = LSldap::formatDate(); + $dn = "reqStart=$now,".LS_ACCESSLOG_BASEDN; + $attrs = array( + 'reqStart' => array($now), + 'reqEnd' => array($now), + 'reqType' => array("delete"), + 'reqSession' => array("1024"), + 'reqAuthzID' => array(LSsession :: get('authenticated_user_dn')), + 'reqDN' => array($data["dn"]), + 'reqResult' => array("0"), + ); + LSldap::getNewEntry($dn, array('auditDelete'), $attrs, true); +} + +function hashPasswordForLogs($password) { + if (preg_match('/^{[^}]+}.*/', $password)) + // Already hashed + return $password; + if(defined('PASSWORD_ARGON2I')) + return '{ARGON2}'.password_hash($password, PASSWORD_ARGON2I); + if(defined('MHASH_SHA512') && function_exists('mhash') && function_exists('mhash_keygen_s2k')) { + mt_srand( (double) microtime() * 1000000 ); + $salt = mhash_keygen_s2k(MHASH_SHA512, $password, substr( pack( "h*", md5( mt_rand() ) ), 0, 8 ), 4 ); + return "{SSHA512}".base64_encode(mhash($mhash_type, $password.$salt).$salt); + } + return '[not logged]'; +} if (php_sapi_name() !== 'cli') { return true; } diff --git a/src/templates/default/showObjectAccessLogs.tpl b/src/templates/default/showObjectAccessLogs.tpl index 055d3b5e..02883014 100644 --- a/src/templates/default/showObjectAccessLogs.tpl +++ b/src/templates/default/showObjectAccessLogs.tpl @@ -3,7 +3,7 @@

{$pagetitle}

{include file='ls:LSview_actions.tpl'} - +
@@ -22,23 +22,23 @@
{tr msg="Date"}{$log.result}{if $log.message} ?{/if} {if $log.mods} - +
- - - - + + + + {foreach $log.mods as $attr => $info} - - - - + + + - {if count($info.changes) > 1} - {section name=change loop=$info.changes step=1 start=1} - - - - - {/section} - {/if} + {if count($info.changes) > 1} + {section name=change loop=$info.changes step=1 start=1} + + + + {/section} + {/if} {/foreach}
{tr msg="Attribute"}{tr msg="Operation"}{tr msg="Value"}{tr msg="Old value(s)"}{tr msg="Operation"}{tr msg="Attribute"}{tr msg="Old value(s)"}{tr msg="Value"}
1}rowspan={$info.changes|count}{/if}>{$attr}{$info.changes.0.op|escape:htmlall}{$info.changes.0.value|escape:htmlall}1}rowspan={$info.changes|count}{/if}> + 1}rowspan={$info.changes|count}{/if}>{$info.changes.0.op|escape:htmlall}1}rowspan={$info.changes|count}{/if}>{$attr}1}rowspan={$info.changes|count}{/if}> {if $info.old_values} +
{if count($info.old_values) == 1} {$info.old_values[0]|escape:'htmlall'} {else} @@ -48,17 +48,18 @@ {/foreach} {/if} +
{/if}
{$info.changes.0.value|escape:htmlall}
{$info.changes[change].op|escape:htmlall}{$info.changes[change].value|escape:htmlall}
{$info.changes[change].value|escape:htmlall}