Add log entries on user login or logout.

This commit is contained in:
Jan Böhmer 2020-01-26 13:59:30 +01:00
parent d6c6b973bf
commit c8375bfa8b
9 changed files with 323 additions and 5 deletions

View file

@ -31,7 +31,6 @@ security:
# https://symfony.com/doc/current/security/form_login_setup.html
form_login:
login_path: login
check_path: login
csrf_token_generator: security.csrf.token_manager
@ -41,6 +40,7 @@ security:
logout:
path: logout
target: homepage
handlers: [App\EventSubscriber\LogoutListener]
remember_me:
secret: '%kernel.secret%'

View file

@ -20,6 +20,8 @@ parameters:
sender_email: 'noreply@partdb.changeme' # The email address from which all emails are sent from
sender_name: 'Part-DB Mailer' # The name that will be used for all mails sent by Part-DB
allow_email_pw_reset: '%env(validMailDSN:MAILER_DSN)%' # Config if users are able, to reset their password by email. By default this enabled, when a mail server is configured.
# If this option is activated, IP addresses are anonymized to be GPDR compliant
gpdr_compliance: true
services:
# default configuration for services in *this* file
@ -28,6 +30,7 @@ services:
autoconfigure: true # Automatically registers your services as commands, event subscribers, etc.
bind:
bool $demo_mode: '%demo_mode%'
bool $gpdr_compliance : '%gpdr_compliance%'
# makes classes in src/ available to be used as services
# this creates a service per class whose id is the fully-qualified class name
@ -47,6 +50,17 @@ services:
$email: '%sender_email%'
$name: '%sender_name%'
App\Services\LogSystem\EventLogger:
arguments:
# By default only log events which has minimum info level (debug levels are not logged)
# 7 is lowest level (debug), 0 highest (emergency
$minimum_log_level: 6
# Event classes specified here are not saved to DB
$blacklist: []
# Only the event classes specified here are saved to DB (set to []) to log all events
$whitelist: []
Liip\ImagineBundle\Service\FilterService:
alias: 'liip_imagine.service.filter'

View file

@ -154,7 +154,7 @@ abstract class AbstractLogEntry extends DBElement
* Get the user that caused the event associated with this log entry.
* @return User
*/
public function getUser(): User
public function getUser(): ?User
{
return $this->user;
}

View file

@ -22,6 +22,7 @@
namespace App\Entity\LogSystem;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\HttpFoundation\IpUtils;
/**
* This log entry is created when a user logs in.
@ -32,6 +33,12 @@ class UserLoginLogEntry extends AbstractLogEntry
{
protected $typeString = "user_login";
public function __construct(string $ip_address, bool $anonymize = true)
{
$this->level = self::LEVEL_INFO;
$this->setIPAddress($ip_address, $anonymize);
}
/**
* Return the (anonymized) IP address used to login the user.
* @return string
@ -44,10 +51,15 @@ class UserLoginLogEntry extends AbstractLogEntry
/**
* Sets the IP address used to login the user
* @param string $ip The IP address used to login the user.
* @param bool $anonymize Anonymize the IP address (remove last block) to be GPDR compliant
* @return $this
*/
public function setIPAddress(string $ip): self
public function setIPAddress(string $ip, bool $anonymize = true): self
{
if ($anonymize) {
$ip = IpUtils::anonymize($ip);
}
$this->extra['i'] = $ip;
return $this;
}

View file

@ -23,6 +23,7 @@ namespace App\Entity\LogSystem;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\HttpFoundation\IpUtils;
/**
* @ORM\Entity()
@ -32,6 +33,12 @@ class UserLogoutLogEntry extends AbstractLogEntry
{
protected $typeString = "user_logout";
public function __construct(string $ip_address, bool $anonymize = true)
{
$this->level = self::LEVEL_INFO;
$this->setIPAddress($ip_address, $anonymize);
}
/**
* Return the (anonymized) IP address used to login the user.
* @return string
@ -44,11 +51,18 @@ class UserLogoutLogEntry extends AbstractLogEntry
/**
* Sets the IP address used to login the user
* @param string $ip The IP address used to login the user.
* @param bool $anonymize Anonymize the IP address (remove last block) to be GPDR compliant
* @return $this
*/
public function setIPAddress(string $ip): self
public function setIPAddress(string $ip, bool $anonymize = true): self
{
if ($anonymize) {
$ip = IpUtils::anonymize($ip);
}
$this->extra['i'] = $ip;
return $this;
}
}

View file

@ -24,6 +24,9 @@ declare(strict_types=1);
namespace App\EventSubscriber;
use App\Entity\LogSystem\UserLoginLogEntry;
use App\Entity\UserSystem\User;
use App\Services\LogSystem\EventLogger;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\Session\Flash\FlashBagInterface;
use Symfony\Component\Security\Http\Event\InteractiveLoginEvent;
@ -37,15 +40,27 @@ final class LoginSuccessListener implements EventSubscriberInterface
{
protected $translator;
protected $flashBag;
protected $eventLogger;
protected $gpdr_compliance;
public function __construct(TranslatorInterface $translator, FlashBagInterface $flashBag)
public function __construct(TranslatorInterface $translator, FlashBagInterface $flashBag, EventLogger $eventLogger, bool $gpdr_compliance)
{
$this->translator = $translator;
$this->flashBag = $flashBag;
$this->eventLogger = $eventLogger;
$this->gpdr_compliance = $gpdr_compliance;
}
public function onLogin(InteractiveLoginEvent $event): void
{
$ip = $event->getRequest()->getClientIp();
$log = new UserLoginLogEntry($ip, $this->gpdr_compliance);
$user = $event->getAuthenticationToken()->getUser();
if ($user instanceof User) {
$log->setTargetElement($user);
}
$this->eventLogger->logAndFlush($log);
$this->flashBag->add('notice', $this->translator->trans('flash.login_successful'));
}

View file

@ -0,0 +1,57 @@
<?php
/**
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2020 Jan Böhmer (https://github.com/jbtronics)
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
*/
namespace App\EventSubscriber;
use App\Entity\LogSystem\UserLogoutLogEntry;
use App\Entity\UserSystem\User;
use App\Services\LogSystem\EventLogger;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Http\Logout\LogoutHandlerInterface;
class LogoutListener implements LogoutHandlerInterface
{
protected $logger;
protected $gpdr_compliance;
public function __construct(EventLogger $logger, bool $gpdr_compliance)
{
$this->logger = $logger;
$this->gpdr_compliance = $gpdr_compliance;
}
/**
* @inheritDoc
*/
public function logout(Request $request, Response $response, TokenInterface $token)
{
$log = new UserLogoutLogEntry($request->getClientIp(), $this->gpdr_compliance);
$user = $token->getUser();
if ($user instanceof User) {
$log->setTargetElement($user);
}
$this->logger->logAndFlush($log);
}
}

View file

@ -0,0 +1,137 @@
<?php
/**
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2020 Jan Böhmer (https://github.com/jbtronics)
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
*/
namespace App\Services\LogSystem;
use App\Entity\LogSystem\AbstractLogEntry;
use App\Entity\UserSystem\User;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Security\Core\Security;
class EventLogger
{
protected $minimum_log_level;
protected $blacklist;
protected $whitelist;
protected $em;
protected $security;
public function __construct(int $minimum_log_level, array $blacklist, array $whitelist, EntityManagerInterface $em, Security $security)
{
$this->minimum_log_level = $minimum_log_level;
$this->blacklist = $blacklist;
$this->whitelist = $whitelist;
$this->em = $em;
$this->security = $security;
}
/**
* Adds the given log entry to the Log, if the entry fullfills the global configured criterias.
* The change will not be flushed yet.
* @param AbstractLogEntry $logEntry
* @return bool Returns true, if the event was added to log.
*/
public function log(AbstractLogEntry $logEntry): bool
{
$user = $this->security->getUser();
//If the user is not specified explicitly, set it to the current user
if (($user === null || $user instanceof User) && $logEntry->getUser() === null) {
if ($user === null) {
$repo = $this->em->getRepository(User::class);
$user = $repo->getAnonymousUser();
}
$logEntry->setUser($user);
}
if ($this->shouldBeAdded($logEntry)) {
$this->em->persist($logEntry);
return true;
}
return false;
}
/**
* Adds the given log entry to the Log, if the entry fullfills the global configured criterias and flush afterwards.
* @param AbstractLogEntry $logEntry
* @return bool Returns true, if the event was added to log.
*/
public function logAndFlush(AbstractLogEntry $logEntry): bool
{
$tmp = $this->log($logEntry);
$this->em->flush();
return $tmp;
}
public function shouldBeAdded(
AbstractLogEntry $logEntry,
?int $minimum_log_level = null,
?array $blacklist = null,
?array $whitelist = null
): bool {
//Apply the global settings, if nothing was specified
$minimum_log_level = $minimum_log_level ?? $this->minimum_log_level;
$blacklist = $blacklist ?? $this->blacklist;
$whitelist = $whitelist ?? $this->whitelist;
//Dont add the entry if it does not reach the minimum level
if ($logEntry->getLevel() > $minimum_log_level) {
return false;
}
//Check if the event type is black listed
if (!empty($blacklist) && $this->isObjectClassInArray($logEntry, $blacklist)) {
return false;
}
//Check for whitelisting
if (!empty($whitelist) && !$this->isObjectClassInArray($logEntry, $whitelist)) {
return false;
}
// By default all things should be added
return true;
}
/**
* Check if the object type is given in the classes array. This also works for inherited types
* @param object $object The object which should be checked
* @param string[] $classes The list of class names that should be used for checking.
* @return bool
*/
protected function isObjectClassInArray(object $object, array $classes): bool
{
//Check if the class is directly in the classes array
if (in_array(get_class($object), $classes)) {
return true;
}
//Iterate over all classes and check for inheritance
foreach ($classes as $class) {
if (is_a($object, $class)) {
return true;
}
}
return false;
}
}

View file

@ -0,0 +1,69 @@
<?php
/**
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2020 Jan Böhmer (https://github.com/jbtronics)
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
*/
namespace App\Tests\Services\LogSystem;
use App\Entity\LogSystem\AbstractLogEntry;
use App\Entity\LogSystem\UserLoginLogEntry;
use App\Entity\LogSystem\UserLogoutLogEntry;
use App\Services\LogSystem\EventLogger;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
class EventLoggerTest extends WebTestCase
{
/**
* @var EventLogger
*/
protected $service;
protected function setUp(): void
{
parent::setUp(); // TODO: Change the autogenerated stub
//Get an service instance.
self::bootKernel();
$this->service = self::$container->get(EventLogger::class);
}
public function testShouldBeAdded()
{
$event1 = new UserLoginLogEntry('127.0.0.1');
$event2 = new UserLogoutLogEntry('127.0.0.1');
$event2->setLevel(AbstractLogEntry::LEVEL_CRITICAL);
//Test without restrictions
$this->assertTrue($this->service->shouldBeAdded($event1, 7, [], []));
//Test minimum log level
$this->assertFalse($this->service->shouldBeAdded($event1, 2, [], []));
$this->assertTrue($this->service->shouldBeAdded($event2, 2, [], []));
//Test blacklist
$this->assertFalse($this->service->shouldBeAdded($event1, 7, [UserLoginLogEntry::class], []));
$this->assertTrue($this->service->shouldBeAdded($event2, 7, [UserLoginLogEntry::class], []));
//Test whitelist
$this->assertFalse($this->service->shouldBeAdded($event1, 7, [], [UserLogoutLogEntry::class]));
$this->assertTrue($this->service->shouldBeAdded($event2, 7, [], [UserLogoutLogEntry::class]));
}
}