Merge pull request #3419 from Icinga/feature/audit-module-2584

Feature/audit module 2584
This commit is contained in:
Eric Lippmann 2018-06-19 05:13:42 -04:00 committed by GitHub
commit 1a841170ff
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 187 additions and 2 deletions

View File

@ -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(

14
doc/15-Auditing.md Normal file
View File

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

View File

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

View File

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

View File

@ -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']);
}
}