Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Anonymisation des comptes agents expiré #2594

Merged
merged 3 commits into from
May 31, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions cron.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@
{
"command": "0 4 * * * php bin/console app:notify-and-archive-inactive-accounts"
},
{
"command": "30 4 * * php bin/console app:anonymize-expired-account"
},
{
"command": "0 1 * * * sh /app/scripts/sync-db.sh"
}
Expand Down
26 changes: 26 additions & 0 deletions migrations/Version20240524151828.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php

declare(strict_types=1);

namespace DoctrineMigrations;

use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;

final class Version20240524151828 extends AbstractMigration
{
public function getDescription(): string
{
return 'Add anonymized_at column to user table';
}

public function up(Schema $schema): void
{
$this->addSql('ALTER TABLE user ADD anonymized_at DATETIME DEFAULT NULL COMMENT \'(DC2Type:datetime_immutable)\'');
}

public function down(Schema $schema): void
{
$this->addSql('ALTER TABLE user DROP anonymized_at');
}
}
71 changes: 71 additions & 0 deletions src/Command/Cron/AnonymizeExpiredAccountCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
<?php

namespace App\Command\Cron;

use App\Entity\User;
use App\Repository\UserRepository;
use App\Service\Mailer\NotificationMail;
use App\Service\Mailer\NotificationMailerRegistry;
use App\Service\Mailer\NotificationMailerType;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;

#[AsCommand(
name: 'app:anonymize-expired-account',
description: 'Sends notifications to inactive accounts and archives them after 30 days'
)]
class AnonymizeExpiredAccountCommand extends AbstractCronCommand
{
private SymfonyStyle $io;

public function __construct(
private readonly EntityManagerInterface $entityManager,
private readonly UserRepository $userRepository,
private readonly NotificationMailerRegistry $notificationMailerRegistry,
private readonly ParameterBagInterface $parameterBag
) {
parent::__construct($this->parameterBag);
}

protected function execute(InputInterface $input, OutputInterface $output): int
{
$this->io = new SymfonyStyle($input, $output);
$nbAgents = $this->anonymizeExpiredUsers();

$this->entityManager->flush();

$message = $nbAgents.' comptes agents expirés anonymisés.';

if ($nbAgents > 0) {
$this->notificationMailerRegistry->send(
new NotificationMail(
type: NotificationMailerType::TYPE_CRON,
to: $this->parameterBag->get('admin_email'),
message: $message,
cronLabel: 'Anonymisation de comptes expirés',
)
);
}

return Command::SUCCESS;
}

private function anonymizeExpiredUsers(): int
{
$expiredUsers = $this->userRepository->findExpiredUsers(true);

/** @var User $user */
foreach ($expiredUsers as $user) {
$user->anonymize();
}

$this->io->success(\count($expiredUsers).' expired users anonymized.');

return \count($expiredUsers);
}
}
6 changes: 4 additions & 2 deletions src/Controller/Back/BackArchivedAccountController.php
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,9 @@ public function reactiver(
): Response {
$isUserUnlinked = (!$user->getTerritory() || !$user->getPartner());

if (User::STATUS_ARCHIVE !== $user->getStatut() && !$isUserUnlinked) {
if ((User::STATUS_ARCHIVE !== $user->getStatut() && !$isUserUnlinked) || $user->getAnonymizedAt()) {
$this->addFlash('error', 'Ce compte ne peut pas être réactivé.');

return $this->redirect($this->generateUrl('back_account_index'));
}

Expand All @@ -103,7 +105,7 @@ public function reactiver(
$form->handleRequest($request);

$untaggedEmail = explode(User::SUFFIXE_ARCHIVED, $user->getEmail())[0];
$userExist = $userRepository->findOneBy(['email' => $untaggedEmail]);
$userExist = $userRepository->findOneByEmailExcepted($untaggedEmail, $user);
if ($userExist) {
$this->addFlash('error', 'Un utilisateur existe déjà avec cette adresse e-mail. '
.$userExist->getNomComplet().' ( id '.$userExist->getId().' ) avec le rôle '
Expand Down
12 changes: 8 additions & 4 deletions src/Controller/Back/PartnerController.php
Original file line number Diff line number Diff line change
Expand Up @@ -502,10 +502,14 @@ public function deleteUser(
territory: $user->getTerritory()
)
);
$user->setEmail(Sanitizer::tagArchivedEmail($user->getEmail()));
$user->setStatut(User::STATUS_ARCHIVE);
$userManager->save($user);
$this->addFlash('success', 'L\'utilisateur a bien été supprimé.');
if (User::STATUS_ARCHIVE === $user->getStatut()) {
$this->addFlash('error', 'Cet utilisateur est déjà supprimé.');
} else {
$user->setEmail(Sanitizer::tagArchivedEmail($user->getEmail()));
$user->setStatut(User::STATUS_ARCHIVE);
$userManager->save($user);
$this->addFlash('success', 'L\'utilisateur a bien été supprimé.');
}

return $this->redirectToRoute(
'back_partner_view',
Expand Down
18 changes: 18 additions & 0 deletions src/DataFixtures/Files/User.yml
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,24 @@ users:
is_generique: 0
is_mailing_active: 1
territory: "Bouches-du-Rhône"
-
email: user-13-08@histologe.fr
roles: "[\"ROLE_USER_PARTNER\"]"
partner: "Partenaire 13-01"
statut: 2
is_generique: 0
is_mailing_active: 1
territory: "Bouches-du-Rhône"
anonymized: true
-
email: user-13-09@histologe.fr
roles: "[\"ROLE_USER_PARTNER\"]"
partner: "Partenaire 13-01"
statut: 2
is_generique: 0
is_mailing_active: 1
territory: "Bouches-du-Rhône"
last_login_at: '-2 year'
-
email: user-2A-01@histologe.fr
roles: "[\"ROLE_USER_PARTNER\"]"
Expand Down
4 changes: 4 additions & 0 deletions src/DataFixtures/Loader/LoadUserData.php
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,10 @@ private function loadUsers(ObjectManager $manager, array $row): void
$user->setLastLoginAt($lastLoginAt);
}

if (isset($row['anonymized']) && $row['anonymized']) {
$user->anonymize();
}

if (isset($row['token'])) {
$user
->setToken($row['token'])
Expand Down
32 changes: 27 additions & 5 deletions src/Entity/User.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@

use App\Entity\Behaviour\TimestampableTrait;
use App\Repository\UserRepository;
use DateTimeImmutable;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\DBAL\Types\Types;
Expand Down Expand Up @@ -41,6 +40,9 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
public const ROLE_ADMIN = self::ROLES['Super Admin'];

public const SUFFIXE_ARCHIVED = '.archived@';
public const ANONYMIZED_MAIL = 'anonyme@';
public const ANONYMIZED_PRENOM = 'Utilisateur';
public const ANONYMIZED_NOM = 'Anonymisé';

public const ROLES = [
'Usager' => 'ROLE_USAGER',
Expand Down Expand Up @@ -136,6 +138,9 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
#[ORM\Column(type: Types::DATE_IMMUTABLE, nullable: true)]
private ?\DateTimeInterface $archivingScheduledAt = null;

#[ORM\Column(nullable: true)]
private ?\DateTimeImmutable $anonymizedAt = null;

public function __construct()
{
$this->suivis = new ArrayCollection();
Expand Down Expand Up @@ -321,7 +326,7 @@ public function getStatutLabel(): string
return self::STATUS_LABELS[$this->statut];
}

public function getLastLoginAt(): ?DateTimeImmutable
public function getLastLoginAt(): ?\DateTimeImmutable
{
return $this->lastLoginAt;
}
Expand All @@ -335,7 +340,7 @@ public function getLastLoginAtStr($format): string
return '';
}

public function setLastLoginAt(?DateTimeImmutable $lastLoginAt): self
public function setLastLoginAt(?\DateTimeImmutable $lastLoginAt): self
{
$this->lastLoginAt = $lastLoginAt;
$this->archivingScheduledAt = null;
Expand Down Expand Up @@ -456,12 +461,12 @@ public function setToken(?string $token): self
return $this;
}

public function getTokenExpiredAt(): ?DateTimeImmutable
public function getTokenExpiredAt(): ?\DateTimeImmutable
{
return $this->tokenExpiredAt;
}

public function setTokenExpiredAt(?DateTimeImmutable $tokenExpiredAt): self
public function setTokenExpiredAt(?\DateTimeImmutable $tokenExpiredAt): self
{
$this->tokenExpiredAt = $tokenExpiredAt;

Expand Down Expand Up @@ -548,4 +553,21 @@ public function setArchivingScheduledAt(?\DateTimeInterface $archivingScheduledA

return $this;
}

public function getAnonymizedAt(): ?\DateTimeImmutable
{
return $this->anonymizedAt;
}

public function anonymize(): static
{
if (self::STATUS_ARCHIVE === $this->getStatut() && null === $this->anonymizedAt) {
$this->setEmail(self::ANONYMIZED_MAIL.date('YmdHis').'.'.uniqid());
$this->setPrenom(self::ANONYMIZED_PRENOM);
$this->setNom(self::ANONYMIZED_NOM);
$this->anonymizedAt = new \DateTimeImmutable();
}

return $this;
}
}
38 changes: 34 additions & 4 deletions src/Repository/UserRepository.php
Original file line number Diff line number Diff line change
Expand Up @@ -61,9 +61,33 @@ public function findArchivedUserByEmail(string $email): ?User

return $queryBuilder
->andWhere('u.email LIKE :email')
->setParameter('email', '%'.$email.'%')
->setParameter('email', $email.'%')
->andWhere('u.statut LIKE :archived')
->setParameter('archived', User::STATUS_ARCHIVE)
->andWhere('u.anonymizedAt IS NULL')
->getQuery()
->getOneOrNullResult();
}

public function findAnonymizedUsers(): ?array
{
$queryBuilder = $this->createQueryBuilder('u');

return $queryBuilder
->andWhere('u.anonymizedAt IS NOT NULL')
->getQuery()
->getResult();
}

public function findOneByEmailExcepted(string $email, User $user): ?User
{
$queryBuilder = $this->createQueryBuilder('u');

return $queryBuilder
->andWhere('u.email = :email')
->setParameter('email', $email)
->andWhere('u.id != :id')
->setParameter('id', $user->getId())
->getQuery()
->getOneOrNullResult();
}
Expand Down Expand Up @@ -136,11 +160,12 @@ public function findAllArchived(
$firstResult = ($page - 1) * $maxResult;

$queryBuilder = $this->createQueryBuilder('u');
$queryBuilder->andWhere('u.anonymizedAt IS NULL');

if ($isNoneTerritory || $isNonePartner) {
if ($isNoneTerritory) {
$queryBuilder
->where('u.territory IS NULL');
->andWhere('u.territory IS NULL');
}
if ($isNonePartner) {
$queryBuilder
Expand All @@ -156,7 +181,7 @@ public function findAllArchived(
}

$queryBuilder
->where('u.statut = :archived'.$builtOrCondition)
->andWhere('u.statut = :archived'.$builtOrCondition)
->setParameter('archived', User::STATUS_ARCHIVE);

if (!empty($territory)) {
Expand Down Expand Up @@ -256,9 +281,13 @@ public function findExpiredUsagers(string $limitConservation = '5 years'): array
return $qb->getQuery()->execute();
}

public function findExpiredUsers(string $limitConservation = '2 years'): array
public function findExpiredUsers(bool $areArchived = false, string $limitConservation = '2 years'): array
{
$qb = $this->getQueryBuilerForinactiveUsersSince($limitConservation);
$qb->andWhere('u.anonymizedAt IS NULL');
if ($areArchived) {
$qb->andWhere('u.statut = :statut')->setParameter('statut', User::STATUS_ARCHIVE);
}

return $qb->getQuery()->execute();
}
Expand All @@ -268,6 +297,7 @@ public function findInactiveUsers(?bool $isArchivingScheduled = null, ?\DateTime
$qb = $this->getQueryBuilerForinactiveUsersSince($limitConservation);

$qb->andWhere('u.statut != :statut')->setParameter('statut', User::STATUS_ARCHIVE);
$qb->andWhere('u.anonymizedAt IS NULL');

if (true === $isArchivingScheduled) {
$qb->andWhere('u.archivingScheduledAt IS NOT NULL');
Expand Down
3 changes: 3 additions & 0 deletions src/Security/Voter/UserVoter.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ protected function voteOnAttribute(string $attribute, $subject, TokenInterface $
if (!$user instanceof UserInterface) {
return false;
}
if ($subject->getAnonymizedAt()) {
return false;
}
if ($this->security->isGranted('ROLE_ADMIN')) {
return true;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php

namespace App\Tests\Functional\Command\Cron;

use Symfony\Bundle\FrameworkBundle\Console\Application;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Symfony\Component\Console\Tester\CommandTester;

class AnonymizeExpiredAccountCommandTest extends KernelTestCase
{
public function testDisplayMessageSuccessfully(): void
{
$kernel = self::bootKernel();
$application = new Application($kernel);

$command = $application->find('app:anonymize-expired-account');

$commandTester = new CommandTester($command);

$commandTester->execute([]);

$commandTester->assertCommandIsSuccessful();

$output = $commandTester->getDisplay();
$this->assertStringContainsString('1 expired users anonymized.', $output);
$this->assertEmailCount(1);
}
}
Loading
Loading