Store sessions in database (#1736)

This commit is contained in:
Kevin Papst
2020-06-11 16:54:51 +02:00
committed by GitHub
parent a75bdc52f6
commit ea4e930cbb
23 changed files with 507 additions and 20 deletions

View File

@@ -11,6 +11,7 @@ Perform EACH version specific task between your version and the new one, otherwi
## [1.10](https://github.com/kevinpapst/kimai2/releases/tag/1.10) ## [1.10](https://github.com/kevinpapst/kimai2/releases/tag/1.10)
- Invoice renderer `CSV` was removed - Invoice renderer `CSV` was removed
- Sessions are now stored in the database (all users have to re-login after upgrade)
### Developer ### Developer

View File

@@ -6,9 +6,12 @@ framework:
# Enables session support. Note that the session will ONLY be started if you read or write from it. # Enables session support. Note that the session will ONLY be started if you read or write from it.
# Remove or comment this section to explicitly disable session support. # Remove or comment this section to explicitly disable session support.
#session:
# handler_id: session.handler.native_file
# save_path: "%kernel.project_dir%/var/sessions/%kernel.environment%"
session: session:
handler_id: session.handler.native_file handler_id: App\Security\SessionHandler
save_path: "%kernel.project_dir%/var/sessions/%kernel.environment%"
#esi: ~ #esi: ~
#fragments: ~ #fragments: ~

View File

@@ -104,6 +104,11 @@ services:
tags: tags:
- { name: doctrine.event_listener, event: postConnect } - { name: doctrine.event_listener, event: postConnect }
# store and retrieve sessions in and from database
App\Security\SessionHandler:
arguments:
- !service { class: PDO, factory: ['@database_connection', 'getWrappedConnection'] }
# ================================================================================ # ================================================================================
# FORMS # FORMS
# ================================================================================ # ================================================================================

View File

@@ -54,9 +54,6 @@ class RedirectToLocaleSubscriber implements EventSubscriberInterface
$this->urlGenerator = $urlGenerator; $this->urlGenerator = $urlGenerator;
$this->locales = explode('|', trim($locales)); $this->locales = explode('|', trim($locales));
if (empty($this->locales)) {
throw new \UnexpectedValueException('The list of supported locales must not be empty.');
}
$this->defaultLocale = $defaultLocale ?: $this->locales[0]; $this->defaultLocale = $defaultLocale ?: $this->locales[0];
if (!\in_array($this->defaultLocale, $this->locales)) { if (!\in_array($this->defaultLocale, $this->locales)) {

View File

@@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace DoctrineMigrations;
use App\Doctrine\AbstractMigration;
use Doctrine\DBAL\Schema\Schema;
/**
* @version 1.10
*/
final class Version20200524142042 extends AbstractMigration
{
public function getDescription(): string
{
return 'Add table to store sessions in database';
}
public function up(Schema $schema): void
{
$sessions = $schema->createTable('kimai2_sessions');
$sessions->addColumn('id', 'string', ['length' => 128, 'notnull' => true]);
$sessions->addColumn('data', 'blob', ['length' => 65535, 'notnull' => true]);
$sessions->addColumn('time', 'integer', ['unsigned' => true, 'notnull' => true]);
$sessions->addColumn('lifetime', 'integer', ['unsigned' => true, 'notnull' => true]);
$sessions->setPrimaryKey(['id']);
}
public function down(Schema $schema): void
{
$schema->dropTable('kimai2_sessions');
}
}

View File

@@ -20,7 +20,6 @@ class PluginManager
/** /**
* @param PluginInterface[] $plugins * @param PluginInterface[] $plugins
* @throws \Exception
*/ */
public function __construct(iterable $plugins) public function __construct(iterable $plugins)
{ {
@@ -31,7 +30,6 @@ class PluginManager
/** /**
* @param PluginInterface $plugin * @param PluginInterface $plugin
* @throws \Exception
*/ */
public function addPlugin(PluginInterface $plugin) public function addPlugin(PluginInterface $plugin)
{ {

View File

@@ -17,11 +17,8 @@ class AclDecisionManager
/** /**
* @var AccessDecisionManagerInterface * @var AccessDecisionManagerInterface
*/ */
protected $decisionManager; private $decisionManager;
/**
* @param AccessDecisionManagerInterface $decisionManager
*/
public function __construct(AccessDecisionManagerInterface $decisionManager) public function __construct(AccessDecisionManagerInterface $decisionManager)
{ {
$this->decisionManager = $decisionManager; $this->decisionManager = $decisionManager;

View File

@@ -0,0 +1,27 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Security;
use Symfony\Component\HttpFoundation\Session\Storage\Handler\PdoSessionHandler;
class SessionHandler extends PdoSessionHandler
{
public function __construct($pdoOrDsn = null)
{
parent::__construct($pdoOrDsn, [
'db_table' => 'kimai2_sessions',
'db_id_col' => 'id',
'db_data_col' => 'data',
'db_lifetime_col' => 'lifetime',
'db_time_col' => 'time',
'lock_mode' => PdoSessionHandler::LOCK_ADVISORY,
]);
}
}

View File

@@ -0,0 +1,39 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Tests\Command;
use App\Command\CreateReleaseCommand;
use Symfony\Bundle\FrameworkBundle\Console\Application;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
/**
* @covers \App\Command\CreateReleaseCommand
* @group integration
*/
class CreateReleaseCommandTest extends KernelTestCase
{
/**
* @var Application
*/
protected $application;
protected function setUp(): void
{
$kernel = self::bootKernel();
$this->application = new Application($kernel);
$this->application->add(new CreateReleaseCommand(realpath(__DIR__ . '/../../')));
}
public function testCommandName()
{
$command = $this->application->find('kimai:create-release');
self::assertInstanceOf(CreateReleaseCommand::class, $command);
}
}

View File

@@ -0,0 +1,51 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Tests\Command;
use App\Command\ImportCustomerCommand;
use App\Configuration\FormConfiguration;
use App\Repository\CustomerRepository;
use App\Repository\ProjectRepository;
use App\Repository\TeamRepository;
use App\Repository\UserRepository;
use Symfony\Bundle\FrameworkBundle\Console\Application;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
/**
* @covers \App\Command\ImportCustomerCommand
* @group integration
*/
class ImportCustomerCommandTest extends KernelTestCase
{
/**
* @var Application
*/
protected $application;
protected function setUp(): void
{
$kernel = self::bootKernel();
$this->application = new Application($kernel);
$customers = $this->createMock(CustomerRepository::class);
$projects = $this->createMock(ProjectRepository::class);
$teams = $this->createMock(TeamRepository::class);
$users = $this->createMock(UserRepository::class);
$configuration = $this->createMock(FormConfiguration::class);
$this->application->add(new ImportCustomerCommand($customers, $projects, $teams, $users, $configuration));
}
public function testCommandName()
{
$command = $this->application->find('kimai:import:customer');
self::assertInstanceOf(ImportCustomerCommand::class, $command);
}
}

View File

@@ -0,0 +1,53 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Tests\Command;
use App\Command\ImportTimesheetCommand;
use App\Configuration\FormConfiguration;
use App\Repository\ActivityRepository;
use App\Repository\CustomerRepository;
use App\Repository\ProjectRepository;
use App\Repository\TimesheetRepository;
use App\Repository\UserRepository;
use Symfony\Bundle\FrameworkBundle\Console\Application;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
/**
* @covers \App\Command\ImportTimesheetCommand
* @group integration
*/
class ImportTimesheetCommandTest extends KernelTestCase
{
/**
* @var Application
*/
protected $application;
protected function setUp(): void
{
$kernel = self::bootKernel();
$this->application = new Application($kernel);
$customers = $this->createMock(CustomerRepository::class);
$projects = $this->createMock(ProjectRepository::class);
$activities = $this->createMock(ActivityRepository::class);
$users = $this->createMock(UserRepository::class);
$timesheets = $this->createMock(TimesheetRepository::class);
$configuration = $this->createMock(FormConfiguration::class);
$this->application->add(new ImportTimesheetCommand($customers, $projects, $activities, $users, $timesheets, $configuration));
}
public function testCommandName()
{
$command = $this->application->find('kimai:import:timesheet');
self::assertInstanceOf(ImportTimesheetCommand::class, $command);
}
}

View File

@@ -0,0 +1,47 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Tests\Command;
use App\Command\KimaiImporterCommand;
use Doctrine\Persistence\ManagerRegistry;
use Symfony\Bundle\FrameworkBundle\Console\Application;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
use Symfony\Component\Validator\Validator\ValidatorInterface;
/**
* @covers \App\Command\KimaiImporterCommand
* @group integration
*/
class KimaiImporterCommandTest extends KernelTestCase
{
/**
* @var Application
*/
protected $application;
protected function setUp(): void
{
$kernel = self::bootKernel();
$this->application = new Application($kernel);
$encoder = $this->createMock(UserPasswordEncoderInterface::class);
$registry = $this->createMock(ManagerRegistry::class);
$validator = $this->createMock(ValidatorInterface::class);
$this->application->add(new KimaiImporterCommand($encoder, $registry, $validator));
}
public function testCommandName()
{
$command = $this->application->find('kimai:import-v1');
self::assertInstanceOf(KimaiImporterCommand::class, $command);
}
}

View File

@@ -0,0 +1,39 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Tests\Command;
use App\Command\ReloadCommand;
use Symfony\Bundle\FrameworkBundle\Console\Application;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
/**
* @covers \App\Command\ReloadCommand
* @group integration
*/
class ReloadCommandTest extends KernelTestCase
{
/**
* @var Application
*/
protected $application;
protected function setUp(): void
{
$kernel = self::bootKernel();
$this->application = new Application($kernel);
$this->application->add(new ReloadCommand());
}
public function testCommandName()
{
$command = $this->application->find('kimai:reload');
self::assertInstanceOf(ReloadCommand::class, $command);
}
}

View File

@@ -0,0 +1,39 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Tests\Command;
use App\Command\ResetCommand;
use Symfony\Bundle\FrameworkBundle\Console\Application;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
/**
* @covers \App\Command\ResetCommand
* @group integration
*/
class ResetCommandTest extends KernelTestCase
{
/**
* @var Application
*/
protected $application;
protected function setUp(): void
{
$kernel = self::bootKernel();
$this->application = new Application($kernel);
$this->application->add(new ResetCommand());
}
public function testCommandName()
{
$command = $this->application->find('kimai:reset-dev');
self::assertInstanceOf(ResetCommand::class, $command);
}
}

View File

@@ -11,7 +11,7 @@ namespace App\Tests\Controller;
use App\Entity\User; use App\Entity\User;
use App\Plugin\PluginManager; use App\Plugin\PluginManager;
use App\Tests\Plugin\Fixtures\TestPlugin; use App\Tests\Plugin\Fixtures\TestPlugin\TestPlugin;
/** /**
* @group integration * @group integration

View File

@@ -0,0 +1,49 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Tests\EventSubscriber;
use App\EventSubscriber\RedirectToLocaleSubscriber;
use PHPUnit\Framework\TestCase;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
/**
* @covers \App\EventSubscriber\RedirectToLocaleSubscriber
*/
class RedirectToLocaleSubscriberTest extends TestCase
{
public function testConstruct()
{
$urlGenerator = $this->createMock(UrlGeneratorInterface::class);
$sut = new RedirectToLocaleSubscriber($urlGenerator, 'de|en', 'en');
self::assertEquals([KernelEvents::REQUEST => ['onKernelRequest']], RedirectToLocaleSubscriber::getSubscribedEvents());
$request = $this->createMock(Request::class);
$request->expects($this->once())->method('getPathInfo')->willReturn('/de');
$event = $this->createMock(RequestEvent::class);
$event->expects($this->once())->method('getRequest')->willReturn($request);
$event->expects($this->never())->method('setResponse');
$sut->onKernelRequest($event);
}
public function testConstructWithUnknownDefaultLocale()
{
$this->expectException(\UnexpectedValueException::class);
$this->expectExceptionMessage('The default locale ("en") must be one of "de|it".');
$urlGenerator = $this->createMock(UrlGeneratorInterface::class);
$sut = new RedirectToLocaleSubscriber($urlGenerator, 'de|it', 'en');
}
}

View File

@@ -7,7 +7,7 @@
* file that was distributed with this source code. * file that was distributed with this source code.
*/ */
namespace App\Tests\Plugin\Fixtures; namespace App\Tests\Plugin\Fixtures\TestPlugin;
use App\Plugin\PluginInterface; use App\Plugin\PluginInterface;

View File

@@ -0,0 +1,31 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Tests\Plugin\Fixtures\TestPlugin2;
use App\Plugin\PluginInterface;
class TestPlugin2 implements PluginInterface
{
/**
* @return string
*/
public function getName()
{
return 'TestPlugin';
}
/**
* @return string
*/
public function getPath()
{
return __DIR__;
}
}

View File

@@ -13,7 +13,8 @@ use App\Plugin\Plugin;
use App\Plugin\PluginInterface; use App\Plugin\PluginInterface;
use App\Plugin\PluginManager; use App\Plugin\PluginManager;
use App\Plugin\PluginMetadata; use App\Plugin\PluginMetadata;
use App\Tests\Plugin\Fixtures\TestPlugin; use App\Tests\Plugin\Fixtures\TestPlugin\TestPlugin;
use App\Tests\Plugin\Fixtures\TestPlugin2\TestPlugin2;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
/** /**
@@ -38,15 +39,13 @@ class PluginManagerTest extends TestCase
{ {
$sut = new PluginManager([]); $sut = new PluginManager([]);
$plugin = $this->getMockBuilder(PluginInterface::class) $plugin = $this->createMock(PluginInterface::class);
->onlyMethods(['getName', 'getPath']) $plugin->expects($this->any())->method('getName')->willReturn('foo');
->getMock(); $plugin->expects($this->any())->method('getPath')->willReturn('bar');
$plugin->method('getName')->willReturn('foo');
$plugin->method('getPath')->willReturn('bar');
$sut->addPlugin(new TestPlugin()); $sut->addPlugin(new TestPlugin());
$sut->addPlugin($plugin); $sut->addPlugin($plugin);
$sut->addPlugin(new TestPlugin2());
$sut->addPlugin(new TestPlugin()); $sut->addPlugin(new TestPlugin());
// make sure a plugin with the same name is not added twice, the first one wins! // make sure a plugin with the same name is not added twice, the first one wins!

View File

@@ -0,0 +1,45 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Tests\Security;
use App\Security\AclDecisionManager;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface;
/**
* @covers \App\Security\AclDecisionManager
*/
class AclDecisionManagerTest extends TestCase
{
public function testFullyAuthenticated()
{
$manager = $this->createMock(AccessDecisionManagerInterface::class);
$manager->expects($this->once())->method('decide')->willReturn(true);
$token = $this->createMock(TokenInterface::class);
$sut = new AclDecisionManager($manager);
$result = $sut->isFullyAuthenticated($token);
self::assertTrue($result);
}
public function testIsNotFullyAuthenticated()
{
$manager = $this->createMock(AccessDecisionManagerInterface::class);
$manager->expects($this->once())->method('decide')->willReturn(false);
$token = $this->createMock(TokenInterface::class);
$sut = new AclDecisionManager($manager);
$result = $sut->isFullyAuthenticated($token);
self::assertFalse($result);
}
}

View File

@@ -0,0 +1,26 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Tests\Security;
use App\Security\SessionHandler;
use PHPUnit\Framework\TestCase;
/**
* @covers \App\Security\SessionHandler
*/
class SessionHandlerTest extends TestCase
{
public function testConstruct()
{
$sut = new SessionHandler(null);
self::assertFalse($sut->isSessionExpired());
}
}

Binary file not shown.