Commit 5c1c3d57 authored by Bertrand Gauthier's avatar Bertrand Gauthier
Browse files

Sécurisation à l'aide d'un témoin d'anonymisation dans une table de metadata ; refactorisations.

parent 4de75e1f
Pipeline #12348 passed with stage
in 15 seconds
......@@ -10,6 +10,8 @@ use UnicaenDbfakator\Controller\IndexController;
use UnicaenDbfakator\Controller\IndexControllerFactory;
use UnicaenDbfakator\Service\DbfakatorService;
use UnicaenDbfakator\Service\DbfakatorServiceFactory;
use UnicaenDbfakator\Service\DbService;
use UnicaenDbfakator\Service\DbServiceFactory;
return [
'router' => [
......@@ -150,6 +152,7 @@ return [
],
'service_manager' => [
'factories' => [
DbService::class => DbServiceFactory::class,
DbfakatorService::class => DbfakatorServiceFactory::class,
],
],
......
--
-- Création si nécessaire de la table des métadonnées de la bdd.
--
create table if not exists _METADATA (
key varchar(64) not null,
value text,
extra text,
description varchar(128)
);
-- Création du témoin d'anonymisation
insert into _METADATA (key, value, description)
values ('BDD_ANONYMISEE', '0', 'Indique si les données de cette bdd ont été anonymisées (1) ou non (0)');
......@@ -25,7 +25,7 @@ class ConsoleController extends AbstractConsoleController
$anonymisationScriptPath = $this->dbfakatorService->getAnonymisationScriptPath();
$restaurationScriptPath = $this->dbfakatorService->getRestaurationScriptPath();
try {
$gen = $this->dbfakatorService->generer($anonymisationScriptPath, $restaurationScriptPath);
$gen = $this->dbfakatorService->generer();
foreach ($gen as ['table' => $table, 'fields' => $fields, 'count' => $count]) {
$this->console->writeLine(
sprintf("- Table %s (colonnes %s) : %d lignes", $table, implode(', ', $fields), $count)
......@@ -49,9 +49,15 @@ class ConsoleController extends AbstractConsoleController
$this->console->writeLine("Lancement du script d'anonymisation '$scriptPath'...");
$start = microtime(true);
$this->lancerAction($scriptPath);
try {
$result = $this->dbfakatorService->anonymiser();
} catch (Exception $e) {
throw new RuntimeException("Une erreur est survenue lors du lancement du script '$scriptPath'.", null, $e);
}
$end = microtime(true);
$this->console->writeLine("> Résultat : " . ($result->isSuccess() ? 'Succès' : 'Échec'));
$this->console->writeLine("> Log file : " . $result->getLogFilePath());
$this->console->writeLine(sprintf("Duree : %.2f s", $end - $start));
}
......@@ -62,21 +68,15 @@ class ConsoleController extends AbstractConsoleController
$this->console->writeLine("Lancement du script de restauration '$scriptPath'...");
$start = microtime(true);
$this->lancerAction($scriptPath);
$end = microtime(true);
$this->console->writeLine(sprintf("Duree : %.2f s", $end - $start));
}
protected function lancerAction(string $scriptPath)
{
try {
$result = $this->dbfakatorService->lancer($scriptPath);
$result = $this->dbfakatorService->restaurer();
} catch (Exception $e) {
throw new RuntimeException("Une erreur est survenue lors du lancement du script '$scriptPath'.", null, $e);
}
$end = microtime(true);
$this->console->writeLine("> Résultat : " . ($result->isSuccess() ? 'Succès' : 'Échec'));
$this->console->writeLine("> Log file : " . $result->getLogFilePath());
$this->console->writeLine(sprintf("Duree : %.2f s", $end - $start));
}
}
\ No newline at end of file
......@@ -3,12 +3,11 @@
namespace UnicaenDbfakator\Controller;
use Application\Controller\AbstractController;
use Exception;
use Laminas\View\Model\ViewModel;
use UnicaenApp\Exception\RuntimeException;
use UnicaenApp\Service\SQL\RunSQLResult;
use UnicaenDbfakator\Service\DbfakatorServiceAwareTrait;
use Exception;
use UnicaenApp\Exception\RuntimeException;
use Webmozart\Assert\InvalidArgumentException;
class IndexController extends AbstractController
{
......@@ -16,9 +15,6 @@ class IndexController extends AbstractController
public function indexAction(): array
{
// $action = $this->params()->fromQuery('action');
// $this->url()->fromRoute('', [], ['query' => ['redirect' => ]], true);
$anonymisationScriptPath = $this->dbfakatorService->getAnonymisationScriptPath();
$restaurationScriptPath = $this->dbfakatorService->getRestaurationScriptPath();
......@@ -41,7 +37,7 @@ class IndexController extends AbstractController
$messages = [];
try {
$gen = $this->dbfakatorService->generer($anonymisationScriptPath, $restaurationScriptPath);
$gen = $this->dbfakatorService->generer();
foreach ($gen as ['table' => $table, 'fields' => $fields, 'count' => $count]) {
$messages[] = sprintf("- Table %s (colonnes %s) : %d lignes", $table, implode(', ', $fields), $count);
}
......@@ -61,7 +57,7 @@ class IndexController extends AbstractController
$scriptPath = $this->dbfakatorService->getAnonymisationScriptPath();
try {
$result = $this->dbfakatorService->lancerAnonymisation($scriptPath);
$result = $this->dbfakatorService->anonymiser();
} catch (Exception $e) {
throw new RuntimeException("Une erreur est survenue lors du lancement du script '$scriptPath'.", null, $e);
}
......@@ -74,7 +70,7 @@ class IndexController extends AbstractController
$scriptPath = $this->dbfakatorService->getRestaurationScriptPath();
try {
$result = $this->dbfakatorService->lancerRestauration($scriptPath);
$result = $this->dbfakatorService->restaurer();
} catch (Exception $e) {
throw new RuntimeException("Une erreur est survenue lors du lancement du script '$scriptPath'.", null, $e);
}
......
<?php
namespace UnicaenDbfakator\Service;
use Doctrine\DBAL\ConnectionException;
use Doctrine\DBAL\Exception;
use Doctrine\ORM\EntityManager;
use Doctrine\ORM\Mapping\ClassMetadata;
use Faker\Factory;
use Laminas\Log\Logger;
use Laminas\Log\Writer\Stream;
use Locale;
use RuntimeException;
use Throwable;
use UnicaenApp\Service\SQL\RunSQLProcess;
use UnicaenApp\Service\SQL\RunSQLResult;
use Webmozart\Assert\Assert;
class DbService
{
const METADATA_KEY_ANONYMISEE = 'BDD_ANONYMISEE';
/**
* @var \Doctrine\ORM\EntityManager
*/
protected $entityManager;
/**
* @var \Doctrine\DBAL\Platforms\AbstractPlatform
*/
protected $databasePlatform;
/**
* @var string
*/
protected $updateSQLTemplate = 'update %s set %s where id = %d ;';
/**
* @var \Faker\Generator $faker
*/
protected $faker;
public function __construct()
{
$this->faker = Factory::create(Locale::getDefault());
}
/**
* @param \Doctrine\ORM\EntityManager $entityManager
* @throws \Doctrine\DBAL\Exception
*/
public function setEntityManager(EntityManager $entityManager): void
{
$this->entityManager = $entityManager;
$this->databasePlatform = $this->entityManager->getConnection()->getDatabasePlatform();
}
public function isBddAnonymisee(): bool
{
$sqlTemplate = "select value from _METADATA where key = '%s'";
$sql = sprintf($sqlTemplate, self::METADATA_KEY_ANONYMISEE);
try {
/** @var string|false $result */
$result = $this->entityManager->getConnection()->executeQuery($sql)->fetchOne();
} catch (Throwable $e) {
throw new RuntimeException(
"Erreur lors de l'interrogation du témoin d'anonymisation dans la table des metadata", null, $e);
}
if ($result === false) {
throw new RuntimeException(sprintf(
"Le témoin d'anonymisation '%s' est introuvable dans la table des metadata",
self::METADATA_KEY_ANONYMISEE
));
}
return $result === '1';
}
public function setBddAnonymisee(bool $bddAnonymisee)
{
$sqlTemplate = "update _METADATA set value = '%d' where key = '%s'";
$sql = sprintf($sqlTemplate, $bddAnonymisee ? '1' : '0', self::METADATA_KEY_ANONYMISEE);
try {
$this->entityManager->getConnection()->executeQuery($sql);
} catch (Exception $e) {
throw new RuntimeException(
"Erreur lors de la modification du témoin d'anonymisation dans la table des metadata", null, $e);
}
}
public function fetchEntityRecords(string $entityName, array $fields, array $except): array
{
$qb = $this->entityManager->createQueryBuilder()
->select("partial t.{id," . implode(',', $fields) . "}")
->from($entityName, 't');
foreach ($except as $field => $values) {
$qb->andWhere($qb->expr()->notIn('t.' . $field, (array)$values));
}
return $qb->getQuery()->getArrayResult();
}
public function getEntityClassMetadata(string $entityName): ClassMetadata
{
return $this->entityManager->getClassMetadata($entityName);
}
public function genUpdateForAnonymiser(array $record, array $mapping, ClassMetadata $metadata): string
{
$tableName = $metadata->getTableName();
$sets = [];
foreach ($mapping as $fieldName => $fakerConfig) {
$fakerConfig = (array)$fakerConfig;
$method = array_shift($fakerConfig);
if ($method === 'null') {
$fakeValue = 'NULL';
} else {
$fakeValue = $this->faker->$method(...$fakerConfig);
$fakeValue = $this->databasePlatform->quoteStringLiteral($fakeValue);
}
$columnName = $metadata->getColumnName($fieldName);
$sets[] = "$columnName = $fakeValue";
}
return sprintf($this->updateSQLTemplate,
$tableName,
implode(', ', $sets),
$record['id']
);
}
public function genUpdateForRestaurer(array $record, array $mapping, ClassMetadata $metadata): string
{
$tableName = $metadata->getTableName();
$sets = [];
foreach ($mapping as $fieldName => $fakerConfig) {
$value = $this->databasePlatform->quoteStringLiteral($record[$fieldName]);
$columnName = $metadata->getColumnName($fieldName);
$sets[] = "$columnName = $value";
}
return sprintf($this->updateSQLTemplate,
$tableName,
implode(', ', $sets),
$record['id']
);
}
/**
* @throws \Doctrine\DBAL\ConnectionException
*/
public function lancerScript(string $scriptPath): RunSQLResult
{
Assert::readable($scriptPath, "Le script '$scriptPath' n'existe pas ou n'est pas lisible");
$conn = $this->entityManager->getConnection();
$conn->beginTransaction();
$p = new RunSQLProcess();
$p->setConnection($conn);
$p->setScriptPath($scriptPath);
$p->setQueriesSplitPattern("#;\n#m"); // point virgule puis un retour
$p->setLogFilePath($scriptPath . '.log');
$p->setLogger((new Logger())->addWriter(new Stream('php://stdout')));
$result = $p->executeScript();
try {
$conn->commit();
} catch (ConnectionException $e) {
$conn->rollBack();
throw $e;
}
return $result;
}
}
\ No newline at end of file
<?php
namespace UnicaenDbfakator\Service;
trait DbServiceAwareTrait
{
/**
* @var DbService $dbService
*/
protected $dbService;
public function setDbService(DbService $dbService)
{
$this->dbService = $dbService;
}
}
\ No newline at end of file
<?php
namespace UnicaenDbfakator\Service;
use Psr\Container\ContainerInterface;
class DbServiceFactory
{
/**
* @param \Psr\Container\ContainerInterface $container
* @return DbService
* @throws \Psr\Container\ContainerExceptionInterface
* @throws \Psr\Container\NotFoundExceptionInterface
* @throws \Doctrine\DBAL\Exception
*/
public function __invoke(ContainerInterface $container): DbService
{
/** @var \Doctrine\ORM\EntityManager $em */
$em = $container->get('doctrine.entitymanager.orm_default');
$service = new DbService;
$service->setEntityManager($em);
return $service;
}
}
\ No newline at end of file
......@@ -2,14 +2,7 @@
namespace UnicaenDbfakator\Service;
use Doctrine\ORM\EntityManager;
use Doctrine\ORM\Mapping\ClassMetadata;
use Faker\Factory;
use Generator;
use Laminas\Log\Logger;
use Laminas\Log\Writer\Stream;
use Locale;
use UnicaenApp\Service\SQL\RunSQLProcess;
use UnicaenApp\Service\SQL\RunSQLResult;
use Webmozart\Assert\Assert;
use Webmozart\Assert\InvalidArgumentException;
......@@ -19,15 +12,7 @@ use Webmozart\Assert\InvalidArgumentException;
*/
class DbfakatorService
{
/**
* Faker
*/
protected $faker;
/**
* @var \Doctrine\ORM\EntityManager
*/
protected $entityManager;
use DbServiceAwareTrait;
/**
* @var array
......@@ -39,24 +24,6 @@ class DbfakatorService
*/
protected $outputConfig = [];
/**
* @var string
*/
protected $updateSQLTemplate = 'update %s set %s where id = %d ;';
/**
* @var \Doctrine\DBAL\Platforms\AbstractPlatform
*/
protected $databasePlatform;
/**
*
*/
public function __construct()
{
$this->faker = Factory::create(Locale::getDefault());
}
/**
* @param array $config
* @return self
......@@ -69,16 +36,6 @@ class DbfakatorService
return $this;
}
/**
* @param \Doctrine\ORM\EntityManager $entityManager
* @throws \Doctrine\DBAL\Exception
*/
public function setEntityManager(EntityManager $entityManager): void
{
$this->entityManager = $entityManager;
$this->databasePlatform = $this->entityManager->getConnection()->getDatabasePlatform();
}
public function getAnonymisationScriptPath()
{
return $this->outputConfig['anonymisation'];
......@@ -89,44 +46,45 @@ class DbfakatorService
return $this->outputConfig['restauration'];
}
public function generer(string $anonymisationSQLFilePath, string $restaurationSQLFilePath): Generator
public function generer(): Generator
{
if (file_exists($anonymisationSQLFilePath)) {
throw new InvalidArgumentException("Le fichier '$anonymisationSQLFilePath' existe déjà !");
if ($this->dbService->isBddAnonymisee()) {
throw new InvalidArgumentException(
"Génération interdite car la base de données est déjà anonymisée (cf. témoin dans la table des metadata)");
}
if (file_exists($restaurationSQLFilePath)) {
throw new InvalidArgumentException("Le fichier '$restaurationSQLFilePath' existe déjà !");
$anonymisationFilePath = $this->getAnonymisationScriptPath();
$restaurationFilePath = $this->getRestaurationScriptPath();
if (file_exists($anonymisationFilePath)) {
throw new InvalidArgumentException("Le fichier '$anonymisationFilePath' existe déjà !");
}
if (file_exists($restaurationFilePath)) {
throw new InvalidArgumentException("Le fichier '$restaurationFilePath' existe déjà !");
}
Assert::notEmpty($this->entitiesConfig, "Aucune classe d'entité trouvée dans la config");
$mf = fopen($anonymisationSQLFilePath, 'w');
$rf = fopen($restaurationSQLFilePath, 'w');
$mf = fopen($anonymisationFilePath, 'w');
$rf = fopen($restaurationFilePath, 'w');
foreach ($this->entitiesConfig as $entityName => $entityConfig) {
Assert::keyExists($entityConfig, $k = 'mapping', "La config de l'entité '$entityName' doit posséder la clé '$k'");
$metadata = $this->entityManager->getClassMetadata($entityName);
$metadata = $this->dbService->getEntityClassMetadata($entityName);
$mapping = $entityConfig['mapping'];
$except = $entityConfig['except'] ?? [];
$fields = array_keys($mapping);
$qb = $this->entityManager->createQueryBuilder()
->select("partial t.{id," . implode(',', $fields) . "}")
->from($entityName, 't');
foreach ($except as $field => $values) {
$qb->andWhere($qb->expr()->notIn('t.' . $field, (array)$values));
}
$records = $qb->getQuery()->getArrayResult();
unset ($qb);
$records = $this->dbService->fetchEntityRecords($entityName, $fields, $except);
$migrateSqlLines = [];
$restaurationSqlLines = [];
foreach ($records as $record) {
$migrateSqlLines[] = $this->genUpdateForAnonymiser($record, $mapping, $metadata);
$restaurationSqlLines[] = $this->genUpdateForRestaurer($record, $mapping, $metadata);
$migrateSqlLines[] = $this->dbService->genUpdateForAnonymiser($record, $mapping, $metadata);
$restaurationSqlLines[] = $this->dbService->genUpdateForRestaurer($record, $mapping, $metadata);
}
fputs($mf, implode(PHP_EOL, $migrateSqlLines) . PHP_EOL);
......@@ -145,70 +103,36 @@ class DbfakatorService
fclose($rf);
}
protected function genUpdateForAnonymiser(array $record, array $mapping, ClassMetadata $metadata): string
/**
* @throws \Doctrine\DBAL\ConnectionException
*/
public function anonymiser(): RunSQLResult
{
$tableName = $metadata->getTableName();
$sets = [];
foreach ($mapping as $fieldName => $fakerConfig) {
$fakerConfig = (array)$fakerConfig;
$method = array_shift($fakerConfig);
if ($method === 'null') {
$fakeValue = 'NULL';
} else {
$fakeValue = $this->faker->$method(...$fakerConfig);
$fakeValue = $this->databasePlatform->quoteStringLiteral($fakeValue);
}
$columnName = $metadata->getColumnName($fieldName);
$sets[] = "$columnName = $fakeValue";
if ($this->dbService->isBddAnonymisee()) {
throw new InvalidArgumentException(
"Anonymisation interdite car la base de données est déjà anonymisée (cf. témoin dans la table des metadata)");
}
return sprintf($this->updateSQLTemplate,
$tableName,
implode(', ', $sets),
$record['id']
);
}
$scriptPath = $this->getAnonymisationScriptPath();
$result = $this->dbService->lancerScript($scriptPath);
$this->dbService->setBddAnonymisee(true);
protected function genUpdateForRestaurer(array $record, array $mapping, ClassMetadata $metadata): string
{
$tableName = $metadata->getTableName();
$sets = [];
foreach ($mapping as $fieldName => $fakerConfig) {
$value = $this->databasePlatform->quoteStringLiteral($record[$fieldName]);
$columnName = $metadata->getColumnName($fieldName);
$sets[] = "$columnName = $value";
}
return sprintf($this->updateSQLTemplate,
$tableName,
implode(', ', $sets),
$record['id']
);
return $result;
}
/**
* @throws \Doctrine\DBAL\ConnectionException
*/
public function lancer($scriptPath): RunSQLResult
public function restaurer(): RunSQLResult
{
Assert::readable($scriptPath, "Le script '$scriptPath' n'existe pas ou n'est pas lisible");
$conn = $this->entityManager->getConnection();
$conn->beginTransaction();
$p = new RunSQLProcess();
$p->setConnection($conn);
$p->setScriptPath($scriptPath);
$p->setQueriesSplitPattern("#;\n#m"); // point virgule puis un retour
$p->setLogFilePath($scriptPath . '.log');
$p->setLogger((new Logger())->addWriter(new Stream('php://stdout')));
$result = $p->executeScript();
if (! $this->dbService->isBddAnonymisee()) {
throw new InvalidArgumentException(
"Restauration interdite car la base de données n'est pas anonymisée (cf. témoin dans la table des metadata)");
}
$conn->commit();
$scriptPath = $this->getRestaurationScriptPath();
$result = $this->dbService->lancerScript($scriptPath);
$this->dbService->setBddAnonymisee(false);
return $result;
}
......
......@@ -10,20 +10,19 @@ class DbfakatorServiceFactory
/**
* @param \Psr\Container\ContainerInterface $container
* @return \UnicaenDbfakator\Service\DbfakatorService
* @throws \Doctrine\DBAL\Exception
* @throws \Psr\Container\ContainerExceptionInterface