Merge pull request #3419 from Icinga/feature/audit-module-2584
Feature/audit module 2584
This commit is contained in:
commit
1a841170ff
|
@ -39,8 +39,8 @@ class LoggingConfigForm extends Form
|
|||
'label' => $this->translate('Logging Type'),
|
||||
'description' => $this->translate('The type of logging to utilize.'),
|
||||
'multiOptions' => array(
|
||||
'php' => $this->translate('Webserver Log', 'app.config.logging.type'),
|
||||
'syslog' => 'Syslog',
|
||||
'php' => $this->translate('Webserver Log', 'app.config.logging.type'),
|
||||
'file' => $this->translate('File', 'app.config.logging.type'),
|
||||
'none' => $this->translate('None', 'app.config.logging.type')
|
||||
)
|
||||
|
@ -94,7 +94,7 @@ class LoggingConfigForm extends Form
|
|||
)
|
||||
);
|
||||
|
||||
if (isset($formData['logging_log']) && $formData['logging_log'] === 'syslog') {
|
||||
if (! isset($formData['logging_log']) || $formData['logging_log'] === 'syslog') {
|
||||
if (Platform::isWindows()) {
|
||||
/* @see https://secure.php.net/manual/en/function.openlog.php */
|
||||
$this->addElement(
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
# Auditing <a id="auditing"></a>
|
||||
|
||||
Auditing in Icinga Web 2 is possible with a separate [module](https://github.com/Icinga/icingaweb2-module-audit).
|
||||
|
||||
This module provides different logging facilities to store/record activities by Icinga Web 2 users.
|
||||
|
||||
Icinga Web 2 currently emits the following activities:
|
||||
|
||||
## Authentication
|
||||
|
||||
Activity | Additional Data
|
||||
---------|----------------
|
||||
login | username
|
||||
logout | username
|
|
@ -0,0 +1,110 @@
|
|||
<?php
|
||||
/* Icinga Web 2 | (c) 2018 Icinga Development Team | GPLv2+ */
|
||||
|
||||
namespace Icinga\Application\Hook;
|
||||
|
||||
use Exception;
|
||||
use InvalidArgumentException;
|
||||
use Icinga\Application\Hook;
|
||||
use Icinga\Application\Logger;
|
||||
|
||||
abstract class AuditHook
|
||||
{
|
||||
/**
|
||||
* Log an activity to the audit log
|
||||
*
|
||||
* Propagates the given message details to all known hook implementations.
|
||||
*
|
||||
* @param string $type An arbitrary name identifying the type of activity
|
||||
* @param string $message A detailed description possibly referencing parameters in $data
|
||||
* @param array $data Additional information (How this is stored or used is up to each implementation)
|
||||
*/
|
||||
public static function logActivity($type, $message, array $data = null)
|
||||
{
|
||||
if (! Hook::has('audit')) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (Hook::all('audit') as $hook) {
|
||||
/** @var self $hook */
|
||||
try {
|
||||
$formattedMessage = $message;
|
||||
if ($data !== null) {
|
||||
// Calling formatMessage on each hook is intended and allows
|
||||
// intercepting message formatting while keeping it implicit
|
||||
$formattedMessage = $hook->formatMessage($message, $data);
|
||||
}
|
||||
|
||||
$hook->logMessage($type, $formattedMessage, $data);
|
||||
} catch (Exception $e) {
|
||||
Logger::error(
|
||||
'Failed to propagate audit message to hook "%s". An error occurred: %s',
|
||||
get_class($hook),
|
||||
$e
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Log a message to the audit log
|
||||
*
|
||||
* @param string $type An arbitrary name identifying the type of activity
|
||||
* @param string $message A detailed description of the activity
|
||||
* @param array $data Additional activity information
|
||||
*/
|
||||
abstract public function logMessage($type, $message, array $data = null);
|
||||
|
||||
/**
|
||||
* Substitute the given message with its accompanying data
|
||||
*
|
||||
* @param string $message
|
||||
* @param array $messageData
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function formatMessage($message, array $messageData)
|
||||
{
|
||||
return preg_replace_callback('/{{(.+?)}}/', function ($match) use ($messageData) {
|
||||
return $this->extractMessageValue(explode('.', $match[1]), $messageData);
|
||||
}, $message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the given value path from the given message data
|
||||
*
|
||||
* @param array $path
|
||||
* @param array $messageData
|
||||
*
|
||||
* @return mixed
|
||||
*
|
||||
* @throws InvalidArgumentException In case of an invalid or missing format parameter
|
||||
*/
|
||||
protected function extractMessageValue(array $path, array $messageData)
|
||||
{
|
||||
$key = array_shift($path);
|
||||
if (array_key_exists($key, $messageData)) {
|
||||
$value = $messageData[$key];
|
||||
} else {
|
||||
throw new InvalidArgumentException("Missing format parameter '$key'");
|
||||
}
|
||||
|
||||
if (empty($path)) {
|
||||
if (! is_scalar($value)) {
|
||||
throw new InvalidArgumentException(
|
||||
'Invalid format parameter. Expected scalar for path "' . join('.', $path) . '".'
|
||||
. ' Got "' . gettype($value) . '" instead'
|
||||
);
|
||||
}
|
||||
|
||||
return $value;
|
||||
} elseif (! is_array($value)) {
|
||||
throw new InvalidArgumentException(
|
||||
'Invalid format parameter. Expected array for path "'. join('.', $path) . '".'
|
||||
. ' Got "' . gettype($value) . '" instead'
|
||||
);
|
||||
}
|
||||
|
||||
return $this->extractMessageValue($path, $value);
|
||||
}
|
||||
}
|
|
@ -5,6 +5,7 @@ namespace Icinga\Authentication;
|
|||
|
||||
use Exception;
|
||||
use Icinga\Application\Config;
|
||||
use Icinga\Application\Hook\AuditHook;
|
||||
use Icinga\Application\Icinga;
|
||||
use Icinga\Application\Logger;
|
||||
use Icinga\Authentication\User\ExternalBackend;
|
||||
|
@ -164,6 +165,7 @@ class Auth
|
|||
if ($persist) {
|
||||
$this->persistCurrentUser();
|
||||
}
|
||||
AuditHook::logActivity('login', 'User {{username}} logged in', ['username' => $user->getUsername()]);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -362,6 +364,7 @@ class Auth
|
|||
*/
|
||||
public function removeAuthorization()
|
||||
{
|
||||
AuditHook::logActivity('logout', 'User {{username}} logged out', ['username' => $this->user->getUsername()]);
|
||||
$this->user = null;
|
||||
Session::getSession()->purge();
|
||||
}
|
||||
|
|
|
@ -0,0 +1,58 @@
|
|||
<?php
|
||||
/* Icinga Web 2 | (c) 2018 Icinga Development Team | GPLv2+ */
|
||||
|
||||
namespace Tests\Icinga\Application\Hook;
|
||||
|
||||
use Icinga\Application\Hook\AuditHook;
|
||||
use Icinga\Test\BaseTestCase;
|
||||
|
||||
class TestAuditHook extends AuditHook
|
||||
{
|
||||
public function logMessage($type, $message, array $data = null)
|
||||
{
|
||||
// TODO: Implement logMessage() method.
|
||||
}
|
||||
}
|
||||
|
||||
class AuditHookTest extends BaseTestCase
|
||||
{
|
||||
public function testFormatMessageResolvesFirstLevelParameters()
|
||||
{
|
||||
$this->assertEquals('foo', (new TestAuditHook())->formatMessage('{{test}}', ['test' => 'foo']));
|
||||
}
|
||||
|
||||
public function testFormatMessageResolvesNestedLevelParameters()
|
||||
{
|
||||
$this->assertEquals('foo', (new TestAuditHook())->formatMessage('{{te.st}}', ['te' => ['st' => 'foo']]));
|
||||
}
|
||||
|
||||
public function testFormatMessageResolvesParametersWithSingleBraces()
|
||||
{
|
||||
$this->assertEquals('foo', (new TestAuditHook())->formatMessage('{{t{e}st}}', ['t{e}st' => 'foo']));
|
||||
$this->assertEquals('foo', (new TestAuditHook())->formatMessage('{{te{.}st}}', ['te{' => ['}st' => 'foo']]));
|
||||
}
|
||||
|
||||
/**
|
||||
* @expectedException \InvalidArgumentException
|
||||
*/
|
||||
public function testFormatMessageComplainsAboutUnresolvedParameters()
|
||||
{
|
||||
(new TestAuditHook())->formatMessage('{{missing}}', []);
|
||||
}
|
||||
|
||||
/**
|
||||
* @expectedException \InvalidArgumentException
|
||||
*/
|
||||
public function testFormatMessageComplainsAboutNonScalarParameters()
|
||||
{
|
||||
(new TestAuditHook())->formatMessage('{{test}}', ['test' => ['foo' => 'bar']]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @expectedException \InvalidArgumentException
|
||||
*/
|
||||
public function testFormatMessageComplainsAboutNonArrayParameters()
|
||||
{
|
||||
(new TestAuditHook())->formatMessage('{{test.foo}}', ['test' => 'foo']);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue