LSaddon accesslog: global improvments and add self logging feature

This commit is contained in:
Benjamin Renard 2023-03-21 10:37:13 +01:00
parent 80a50f98f1
commit 13d83dbf75
Signed by: bn8
GPG key ID: 3E2E1CE1907115BC
4 changed files with 286 additions and 69 deletions

View file

@ -23,3 +23,14 @@
// Accesslog base DN // Accesslog base DN
define('LS_ACCESSLOG_BASEDN', 'cn=ldapsaisie-accesslog'); 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);

View file

@ -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;
}

View file

@ -25,29 +25,36 @@ LSerror :: defineError('ACCESSLOG_SUPPORT_01',
); );
$GLOBALS['accesslog_reqTypes'] = array( $GLOBALS['accesslog_reqTypes'] = array(
'add' => _('Add'), 'add' => ___('Add'),
'bind' => _('Log in'), 'bind' => ___('Log in'),
'compare' => _('Compare'), 'compare' => ___('Compare'),
'delete' => _('Delete'), 'delete' => ___('Delete'),
'extended' => _('Extended'), 'extended' => ___('Extended'),
'modify' => _('Modify'), 'modify' => ___('Modify'),
'modrdn' => _('Modify RDN'), 'modrdn' => ___('Modify RDN'),
'search' => _('Search'), 'search' => ___('Search'),
'unbind' => _('Log out'), 'unbind' => ___('Log out'),
); );
$GLOBALS['accesslog_modOps'] = array( $GLOBALS['accesslog_modOps'] = array(
'+' => _('Add'), '+' => ___('Add'),
'-' => _('Delete'), '-' => ___('Delete'),
'=' => _('Replace'), '=' => ___('Replace'),
'' => _('Replace'), '' => ___('Replace'),
'#' => _('Increment'), '#' => ___('Increment'),
); );
function LSaddon_accesslog_support() { function LSaddon_accesslog_support() {
if (!defined('LS_ACCESSLOG_BASEDN')) { $MUST_DEFINE_CONST= array(
LSerror :: addErrorCode('ACCESSLOG_SUPPORT_01', 'LS_ACCESSLOG_BASEDN'); 'LS_ACCESSLOG_BASEDN',
return false; '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') { if (php_sapi_name() === 'cli') {
LScli::add_command( LScli::add_command(
@ -57,10 +64,18 @@ function LSaddon_accesslog_support() {
'[entry DN] [page]' '[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; return true;
} }
function mapAccessLogEntry(&$entry) { function mapAccessLogEntry(&$entry) {
foreach($entry['attrs'] as $attr => $values)
$entry['attrs'][$attr] = ensureIsArray($values);
$attrs = $entry['attrs']; $attrs = $entry['attrs'];
$entry['start'] = LSldap::parseDate(LSldap::getAttr($attrs, 'reqStart')); $entry['start'] = LSldap::parseDate(LSldap::getAttr($attrs, 'reqStart'));
$entry['end'] = LSldap::parseDate(LSldap::getAttr($attrs, 'reqEnd')); $entry['end'] = LSldap::parseDate(LSldap::getAttr($attrs, 'reqEnd'));
@ -73,49 +88,67 @@ function mapAccessLogEntry(&$entry) {
$entry['type'] = LSldap::getAttr($attrs, 'reqType'); $entry['type'] = LSldap::getAttr($attrs, 'reqType');
$entry['result'] = ldap_err2str(LSldap::getAttr($attrs, 'reqResult')); $entry['result'] = ldap_err2str(LSldap::getAttr($attrs, 'reqResult'));
$entry['message'] = LSldap::getAttr($attrs, 'reqMessage'); $entry['message'] = LSldap::getAttr($attrs, 'reqMessage');
if ($entry['type'] === 'modify' && LSldap::getAttr($attrs, 'reqMod', true)) { $mods = array();
$mods = array(); foreach(LSldap::getAttr($attrs, 'reqMod', true) as $mod) {
foreach(LSldap::getAttr($attrs, 'reqMod', true) as $mod) { if (preg_match('/^(?P<attr>[^\:]+)\:(?P<op>[^ ]?)( (?P<value>.*))?$/', $mod, $m)) {
if (preg_match('/^([^\:]+)\:([^ ]?) (.*)$/', $mod, $m)) { $attr = $m['attr'];
$attr = $m[1]; $op = $m['op'];
$op = $m[2]; $value = isset($m['value'])?$m['value']:null;
$value = $m[3]; if (!array_key_exists($attr, $mods)) {
if (!array_key_exists($attr, $mods)) { $mods[$attr] = array(
$mods[$attr] = array( 'changes' => array(),
'mods' => array(), 'old_values' => array(),
'old_values' => array(),
);
}
$mods[$attr]['changes'][] = array(
'op' => array_key_exists($op, $GLOBALS['accesslog_modOps']) ? $GLOBALS['accesslog_modOps'][$op] : $op,
'value' => $value,
); );
} }
$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 (LSldap::getAttr($attrs, 'reqOld', true)) {
if (preg_match('/^([^\:]+)\: (.*)$/', $old, $m) && array_key_exists($m[1], $mods)) { foreach(LSldap::getAttr($attrs, 'reqOld', true) as $old) {
$mods[$m[1]]['old_values'][] = $m[2]; if (preg_match('/^([^\:]+)\: (.*)$/', $old, $m) && array_key_exists($m[1], $mods)) {
} $mods[$m[1]]['old_values'][] = $m[2];
} }
} }
}
if ($mods)
$entry['mods'] = $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'])) { 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) { function sortLogEntriesByDate(&$a, &$b) {
return ($a['start'] === $b['start']) ? 0 : ($a['start'] < $b['start']) ? -1 : 1; $astart = LSldap::getAttr($a['attrs'], 'reqStart');
$bstart = LSldap::getAttr($b['attrs'], 'reqStart');
return ($astart === $bstart) ? 0 : ($astart < $bstart) ? -1 : 1;
} }
function getEntryAccessLog($dn) { function getEntryAccessLog($dn, $start_date=null) {
$data = LSldap::search( $filter = Net_LDAP2_Filter::create('reqDn', 'equals', $dn);
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, LS_ACCESSLOG_BASEDN,
array( array(
'attributes' => array( 'attributes' => array(
'reqDN',
'reqStart', 'reqStart',
'reqEnd', 'reqEnd',
'reqAuthzID', 'reqAuthzID',
@ -124,22 +157,33 @@ function getEntryAccessLog($dn) {
'reqMessage', 'reqMessage',
'reqMod', 'reqMod',
'reqOld', 'reqOld',
'reqNewRDN',
'reqNewSuperior',
), ),
) )
); );
if (!is_array($data)) { if (!is_array($entries)) {
return; return;
} }
usort($entries, 'sortLogEntriesByDate');
$logs = array(); $logs = array();
foreach($data as $entry) { $new_dn = null;
foreach($entry['attrs'] as $attr => $values) { $rename_date = null;
$entry['attrs'][$attr] = ensureIsArray($values); foreach($entries as $entry) {
}
mapAccessLogEntry($entry); mapAccessLogEntry($entry);
$logs[] = $entry; $logs[] = $entry;
if (isset($entry['new_dn'])) {
$new_dn = $entry['new_dn'];
$rename_date = LSldap::formatDate($entry['start']);
break;
}
} }
usort($logs, 'sortLogEntryByDate'); if ($new_dn) {
return array_reverse($logs); $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) { function getEntryAccessLogPage($dn, $page = false, $nbByPage = 30) {
@ -183,11 +227,136 @@ function showObjectAccessLogs($obj) {
'action' => 'view', 'action' => 'view',
); );
LStemplate::assign('LSview_actions', $LSview_actions); LStemplate::assign('LSview_actions', $LSview_actions);
LStemplate::addCSSFile('showObjectAccessLogs.css');
LSsession::setTemplate('showObjectAccessLogs.tpl'); LSsession::setTemplate('showObjectAccessLogs.tpl');
LSsession::displayTemplate(); LSsession::displayTemplate();
exit(); 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') { if (php_sapi_name() !== 'cli') {
return true; return true;
} }

View file

@ -3,7 +3,7 @@
<h1>{$pagetitle}</h1> <h1>{$pagetitle}</h1>
{include file='ls:LSview_actions.tpl'} {include file='ls:LSview_actions.tpl'}
<table class='LStable'> <table class='LStable objectAccessLogs'>
<thead> <thead>
<tr> <tr>
<th>{tr msg="Date"}</th> <th>{tr msg="Date"}</th>
@ -22,23 +22,23 @@
<td class="center">{$log.result}{if $log.message} <img class='LStips' src="{img name='help'}" alt="?" title='{$log.message|escape:quotes}'/>{/if}</td> <td class="center">{$log.result}{if $log.message} <img class='LStips' src="{img name='help'}" alt="?" title='{$log.message|escape:quotes}'/>{/if}</td>
<td> <td>
{if $log.mods} {if $log.mods}
<table style='margin: auto'> <table class="mods">
<thead> <thead>
<tr> <tr>
<th>{tr msg="Attribute"}</th> <th class="col-op">{tr msg="Operation"}</th>
<th>{tr msg="Operation"}</th> <th class="col-attr">{tr msg="Attribute"}</th>
<th>{tr msg="Value"}</th> <th class="col-value">{tr msg="Old value(s)"}</th>
<th>{tr msg="Old value(s)"}</th> <th class="col-value">{tr msg="Value"}</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{foreach $log.mods as $attr => $info} {foreach $log.mods as $attr => $info}
<tr> <tr>
<td class="center" {if count($info.changes)>1}rowspan={$info.changes|count}{/if}>{$attr}</td> <td class="col-op center" {if count($info.changes)>1}rowspan={$info.changes|count}{/if}>{$info.changes.0.op|escape:htmlall}</td>
<td class="center">{$info.changes.0.op|escape:htmlall}</td> <td class="col-attr center" {if count($info.changes)>1}rowspan={$info.changes|count}{/if}>{$attr}</td>
<td>{$info.changes.0.value|escape:htmlall}</td> <td class="col-value" {if count($info.changes)>1}rowspan={$info.changes|count}{/if}>
<td {if count($info.changes)>1}rowspan={$info.changes|count}{/if}>
{if $info.old_values} {if $info.old_values}
<div class="copyable copyable-no-btn">
{if count($info.old_values) == 1} {if count($info.old_values) == 1}
{$info.old_values[0]|escape:'htmlall'} {$info.old_values[0]|escape:'htmlall'}
{else} {else}
@ -48,17 +48,18 @@
{/foreach} {/foreach}
</ul> </ul>
{/if} {/if}
</div>
{/if} {/if}
</td> </td>
<td class="col-value"><div class="copyable copyable-no-btn">{$info.changes.0.value|escape:htmlall}</div></td>
</tr> </tr>
{if count($info.changes) > 1} {if count($info.changes) > 1}
{section name=change loop=$info.changes step=1 start=1} {section name=change loop=$info.changes step=1 start=1}
<tr> <tr>
<td>{$info.changes[change].op|escape:htmlall}</td> <td class="col-value"><div class="copyable copyable-no-btn">{$info.changes[change].value|escape:htmlall}</div></td>
<td>{$info.changes[change].value|escape:htmlall}</td> </tr>
</tr> {/section}
{/section} {/if}
{/if}
{/foreach} {/foreach}
</tbody> </tbody>
</table> </table>