fix(absences) : durcissement RGPD des données RH des utilisateurs
Suite à la revue de conformité du module absences.
Fuite corrigée : GET /api/users et /api/users/{id} n'avaient aucun contrôle
d'accès alors que le groupe user:list exposait les données RH/familiales
(date d'embauche, contrat, soldes de CP, rôles…). Tout utilisateur authentifié
pouvait donc lire ces informations sur tous ses collègues.
- chaque champ RH (isEmployee, hireDate, endDate, contractType, workTimeRatio,
annualLeaveDays, referencePeriodStart, initialLeaveBalance) ainsi que roles
est désormais exposé via #[ApiProperty(security: "is_granted('ROLE_ADMIN') or
object == user")] : visible uniquement par un admin ou par l'utilisateur
lui-même. id et username restent publics (sélecteurs d'assigné, avatars).
Minimisation : suppression de familySituation et nbChildren, collectés et
exposés (form RH, API, outil MCP) mais utilisés par aucun calcul.
- entité User + enum FamilySituation + migration de drop des colonnes
- Serializer MCP, update-user (MCP), EmployeeDrawer, DTO, fixtures, i18n
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -24,13 +24,6 @@
|
||||
empty-option-label="—"
|
||||
group-class="w-full"
|
||||
/>
|
||||
<MalioSelect
|
||||
v-model="form.familySituation"
|
||||
:label="$t('absences.admin.employees.fields.familySituation')"
|
||||
:options="familyOptions"
|
||||
empty-option-label="—"
|
||||
group-class="w-full"
|
||||
/>
|
||||
<MalioInputText
|
||||
v-model="form.workTimeRatio"
|
||||
:label="$t('absences.admin.employees.fields.workTimeRatio')"
|
||||
@@ -51,11 +44,6 @@
|
||||
:label="$t('absences.admin.employees.fields.initialLeaveBalance')"
|
||||
input-class="w-full"
|
||||
/>
|
||||
<MalioInputText
|
||||
v-model="form.nbChildren"
|
||||
:label="$t('absences.admin.employees.fields.nbChildren')"
|
||||
input-class="w-full"
|
||||
/>
|
||||
|
||||
<div class="col-span-full mt-2 flex justify-end">
|
||||
<MalioButton
|
||||
@@ -70,7 +58,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { ContractType, FamilySituation, UserData } from '~/services/dto/user-data'
|
||||
import type { ContractType, UserData } from '~/services/dto/user-data'
|
||||
import { useUserService } from '~/services/users'
|
||||
|
||||
const props = defineProps<{
|
||||
@@ -101,24 +89,14 @@ const contractOptions = [
|
||||
{ label: t('absences.admin.employees.contract.autre'), value: 'AUTRE' },
|
||||
]
|
||||
|
||||
const familyOptions = [
|
||||
{ label: t('absences.admin.employees.family.celibataire'), value: 'CELIBATAIRE' },
|
||||
{ label: t('absences.admin.employees.family.marie'), value: 'MARIE' },
|
||||
{ label: t('absences.admin.employees.family.pacse'), value: 'PACSE' },
|
||||
{ label: t('absences.admin.employees.family.divorce'), value: 'DIVORCE' },
|
||||
{ label: t('absences.admin.employees.family.veuf'), value: 'VEUF' },
|
||||
]
|
||||
|
||||
const form = reactive({
|
||||
hireDate: null as string | null,
|
||||
endDate: null as string | null,
|
||||
contractType: null as ContractType | null,
|
||||
familySituation: null as FamilySituation | null,
|
||||
workTimeRatio: '1.0',
|
||||
annualLeaveDays: '25',
|
||||
referencePeriodStart: '06-01',
|
||||
initialLeaveBalance: '0',
|
||||
nbChildren: '0',
|
||||
})
|
||||
|
||||
function hydrate(u: UserData | null) {
|
||||
@@ -126,12 +104,10 @@ function hydrate(u: UserData | null) {
|
||||
form.hireDate = u.hireDate ? u.hireDate.slice(0, 10) : null
|
||||
form.endDate = u.endDate ? u.endDate.slice(0, 10) : null
|
||||
form.contractType = u.contractType ?? null
|
||||
form.familySituation = u.familySituation ?? null
|
||||
form.workTimeRatio = String(u.workTimeRatio ?? 1)
|
||||
form.annualLeaveDays = String(u.annualLeaveDays ?? 25)
|
||||
form.referencePeriodStart = u.referencePeriodStart ?? '06-01'
|
||||
form.initialLeaveBalance = String(u.initialLeaveBalance ?? 0)
|
||||
form.nbChildren = String(u.nbChildren ?? 0)
|
||||
}
|
||||
|
||||
watch(() => props.modelValue, (isOpen) => {
|
||||
@@ -147,12 +123,10 @@ async function save() {
|
||||
hireDate: form.hireDate || null,
|
||||
endDate: form.endDate || null,
|
||||
contractType: form.contractType,
|
||||
familySituation: form.familySituation,
|
||||
workTimeRatio: Number(form.workTimeRatio) || 1,
|
||||
annualLeaveDays: Number(form.annualLeaveDays) || 0,
|
||||
referencePeriodStart: form.referencePeriodStart || '06-01',
|
||||
initialLeaveBalance: Number(form.initialLeaveBalance) || 0,
|
||||
nbChildren: Number(form.nbChildren) || 0,
|
||||
})
|
||||
emit('saved')
|
||||
open.value = false
|
||||
|
||||
@@ -703,12 +703,10 @@
|
||||
"hireDate": "Date d'embauche",
|
||||
"endDate": "Date de sortie",
|
||||
"contractType": "Type de contrat",
|
||||
"familySituation": "Situation familiale",
|
||||
"workTimeRatio": "Temps de travail (ex : 1.0)",
|
||||
"annualLeaveDays": "CP annuels (jours)",
|
||||
"referencePeriodStart": "Début période réf. (MM-DD)",
|
||||
"initialLeaveBalance": "Solde CP initial",
|
||||
"nbChildren": "Nombre d'enfants"
|
||||
"initialLeaveBalance": "Solde CP initial"
|
||||
},
|
||||
"contract": {
|
||||
"cdi": "CDI",
|
||||
@@ -716,13 +714,6 @@
|
||||
"stage": "Stage",
|
||||
"alternance": "Alternance",
|
||||
"autre": "Autre"
|
||||
},
|
||||
"family": {
|
||||
"celibataire": "Célibataire",
|
||||
"marie": "Marié(e)",
|
||||
"pacse": "Pacsé(e)",
|
||||
"divorce": "Divorcé(e)",
|
||||
"veuf": "Veuf(ve)"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
export type ContractType = 'CDI' | 'CDD' | 'STAGE' | 'ALTERNANCE' | 'AUTRE'
|
||||
export type FamilySituation = 'CELIBATAIRE' | 'MARIE' | 'PACSE' | 'DIVORCE' | 'VEUF'
|
||||
|
||||
export type UserData = {
|
||||
id: number
|
||||
@@ -17,8 +16,6 @@ export type UserData = {
|
||||
annualLeaveDays?: number
|
||||
referencePeriodStart?: string
|
||||
initialLeaveBalance?: number
|
||||
familySituation?: FamilySituation | null
|
||||
nbChildren?: number
|
||||
}
|
||||
|
||||
export type UserWrite = {
|
||||
@@ -34,6 +31,4 @@ export type UserWrite = {
|
||||
annualLeaveDays?: number
|
||||
referencePeriodStart?: string
|
||||
initialLeaveBalance?: number
|
||||
familySituation?: FamilySituation | null
|
||||
nbChildren?: number
|
||||
}
|
||||
|
||||
33
migrations/Version20260522140000.php
Normal file
33
migrations/Version20260522140000.php
Normal file
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* RGPD / minimisation : suppression des colonnes user.family_situation et
|
||||
* user.nb_children. Ces données (situation familiale, nombre d'enfants)
|
||||
* étaient collectées et exposées mais n'étaient utilisées par aucun calcul.
|
||||
*/
|
||||
final class Version20260522140000 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Drop unused HR fields user.family_situation and user.nb_children (data minimisation)';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$this->addSql('ALTER TABLE "user" DROP COLUMN IF EXISTS family_situation');
|
||||
$this->addSql('ALTER TABLE "user" DROP COLUMN IF EXISTS nb_children');
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$this->addSql('ALTER TABLE "user" ADD family_situation VARCHAR(16) DEFAULT NULL');
|
||||
$this->addSql('ALTER TABLE "user" ADD nb_children INT NOT NULL DEFAULT 0');
|
||||
}
|
||||
}
|
||||
@@ -24,7 +24,6 @@ use App\Entity\ZimbraConfiguration;
|
||||
use App\Enum\AbsenceStatus;
|
||||
use App\Enum\AbsenceType;
|
||||
use App\Enum\ContractType;
|
||||
use App\Enum\FamilySituation;
|
||||
use App\Enum\RecurrenceType;
|
||||
use App\Enum\StatusCategory;
|
||||
use DateTimeImmutable;
|
||||
@@ -670,20 +669,15 @@ class AppFixtures extends Fixture
|
||||
$admin->setIsEmployee(true);
|
||||
$admin->setHireDate(new DateTimeImmutable('2020-01-15'));
|
||||
$admin->setContractType(ContractType::Cdi);
|
||||
$admin->setFamilySituation(FamilySituation::Married);
|
||||
$admin->setNbChildren(2);
|
||||
|
||||
$userAlice->setIsEmployee(true);
|
||||
$userAlice->setHireDate(new DateTimeImmutable('2022-09-01'));
|
||||
$userAlice->setContractType(ContractType::Cdi);
|
||||
$userAlice->setFamilySituation(FamilySituation::Single);
|
||||
|
||||
$userBob->setIsEmployee(true);
|
||||
$userBob->setHireDate(new DateTimeImmutable('2023-03-10'));
|
||||
$userBob->setContractType(ContractType::Cdd);
|
||||
$userBob->setWorkTimeRatio(0.8);
|
||||
$userBob->setFamilySituation(FamilySituation::Pacsed);
|
||||
$userBob->setNbChildren(1);
|
||||
|
||||
// Paid-leave balances for the current reference period (June 1st → May 31st)
|
||||
$cpPeriod = '2025-2026';
|
||||
|
||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use ApiPlatform\Metadata\ApiProperty;
|
||||
use ApiPlatform\Metadata\ApiResource;
|
||||
use ApiPlatform\Metadata\Delete;
|
||||
use ApiPlatform\Metadata\Get;
|
||||
@@ -11,7 +12,6 @@ use ApiPlatform\Metadata\GetCollection;
|
||||
use ApiPlatform\Metadata\Patch;
|
||||
use ApiPlatform\Metadata\Post;
|
||||
use App\Enum\ContractType;
|
||||
use App\Enum\FamilySituation;
|
||||
use App\Repository\UserRepository;
|
||||
use App\State\MeProvider;
|
||||
use App\State\UserPasswordHasherProcessor;
|
||||
@@ -57,6 +57,7 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
|
||||
|
||||
/** @var list<string> */
|
||||
#[ORM\Column]
|
||||
#[ApiProperty(security: "is_granted('ROLE_ADMIN') or object == user")]
|
||||
#[Groups(['me:read', 'user:list', 'user:write'])]
|
||||
private array $roles = [];
|
||||
|
||||
@@ -76,54 +77,54 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
|
||||
#[ORM\Column(length: 255, nullable: true)]
|
||||
private ?string $avatarFileName = null;
|
||||
|
||||
// --- HR / absence management fields ---
|
||||
// --- HR / absence management fields (readable only by an admin or the user themselves) ---
|
||||
|
||||
/** Whether this user is an employee subject to absence management. */
|
||||
#[ORM\Column]
|
||||
#[ApiProperty(security: "is_granted('ROLE_ADMIN') or object == user")]
|
||||
#[Groups(['me:read', 'user:list', 'user:write'])]
|
||||
private bool $isEmployee = false;
|
||||
|
||||
/** Hiring date — start of paid-leave acquisition. */
|
||||
#[ORM\Column(type: Types::DATE_IMMUTABLE, nullable: true)]
|
||||
#[ApiProperty(security: "is_granted('ROLE_ADMIN') or object == user")]
|
||||
#[Groups(['me:read', 'user:list', 'user:write'])]
|
||||
private ?DateTimeImmutable $hireDate = null;
|
||||
|
||||
#[ORM\Column(type: Types::DATE_IMMUTABLE, nullable: true)]
|
||||
#[ApiProperty(security: "is_granted('ROLE_ADMIN') or object == user")]
|
||||
#[Groups(['me:read', 'user:list', 'user:write'])]
|
||||
private ?DateTimeImmutable $endDate = null;
|
||||
|
||||
#[ORM\Column(type: Types::STRING, length: 16, nullable: true, enumType: ContractType::class)]
|
||||
#[ApiProperty(security: "is_granted('ROLE_ADMIN') or object == user")]
|
||||
#[Groups(['me:read', 'user:list', 'user:write'])]
|
||||
private ?ContractType $contractType = null;
|
||||
|
||||
/** Work-time ratio: 1.0 = full time, 0.8 = 4 days out of 5. */
|
||||
#[ORM\Column(type: Types::FLOAT)]
|
||||
#[ApiProperty(security: "is_granted('ROLE_ADMIN') or object == user")]
|
||||
#[Groups(['me:read', 'user:list', 'user:write'])]
|
||||
private float $workTimeRatio = 1.0;
|
||||
|
||||
/** Yearly paid-leave entitlement in worked days (default 25 = jours ouvrés). */
|
||||
#[ORM\Column(type: Types::FLOAT)]
|
||||
#[ApiProperty(security: "is_granted('ROLE_ADMIN') or object == user")]
|
||||
#[Groups(['me:read', 'user:list', 'user:write'])]
|
||||
private float $annualLeaveDays = 25.0;
|
||||
|
||||
/** Reference period start as MM-DD (default 06-01, 1st of June). */
|
||||
#[ORM\Column(length: 5)]
|
||||
#[ApiProperty(security: "is_granted('ROLE_ADMIN') or object == user")]
|
||||
#[Groups(['me:read', 'user:list', 'user:write'])]
|
||||
private string $referencePeriodStart = '06-01';
|
||||
|
||||
/** Paid-leave already acquired when the module is rolled out. */
|
||||
#[ORM\Column(type: Types::FLOAT)]
|
||||
#[ApiProperty(security: "is_granted('ROLE_ADMIN') or object == user")]
|
||||
#[Groups(['me:read', 'user:list', 'user:write'])]
|
||||
private float $initialLeaveBalance = 0.0;
|
||||
|
||||
#[ORM\Column(type: Types::STRING, length: 16, nullable: true, enumType: FamilySituation::class)]
|
||||
#[Groups(['me:read', 'user:list', 'user:write'])]
|
||||
private ?FamilySituation $familySituation = null;
|
||||
|
||||
#[ORM\Column]
|
||||
#[Groups(['me:read', 'user:list', 'user:write'])]
|
||||
private int $nbChildren = 0;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->createdAt = new DateTimeImmutable();
|
||||
@@ -338,28 +339,4 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getFamilySituation(): ?FamilySituation
|
||||
{
|
||||
return $this->familySituation;
|
||||
}
|
||||
|
||||
public function setFamilySituation(?FamilySituation $familySituation): static
|
||||
{
|
||||
$this->familySituation = $familySituation;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getNbChildren(): int
|
||||
{
|
||||
return $this->nbChildren;
|
||||
}
|
||||
|
||||
public function setNbChildren(int $nbChildren): static
|
||||
{
|
||||
$this->nbChildren = $nbChildren;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Enum;
|
||||
|
||||
enum FamilySituation: string
|
||||
{
|
||||
case Single = 'CELIBATAIRE';
|
||||
case Married = 'MARIE';
|
||||
case Pacsed = 'PACSE';
|
||||
case Divorced = 'DIVORCE';
|
||||
case Widowed = 'VEUF';
|
||||
|
||||
public function label(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::Single => 'Célibataire',
|
||||
self::Married => 'Marié(e)',
|
||||
self::Pacsed => 'Pacsé(e)',
|
||||
self::Divorced => 'Divorcé(e)',
|
||||
self::Widowed => 'Veuf(ve)',
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,6 @@ declare(strict_types=1);
|
||||
namespace App\Mcp\Tool\Reference;
|
||||
|
||||
use App\Enum\ContractType;
|
||||
use App\Enum\FamilySituation;
|
||||
use App\Mcp\Tool\Serializer;
|
||||
use App\Repository\UserRepository;
|
||||
use DateTimeImmutable;
|
||||
@@ -17,7 +16,7 @@ use Symfony\Component\Security\Core\Exception\AccessDeniedException;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
#[McpTool(name: 'update-user', description: 'Update a user HR/profile fields (admin). Does NOT change password or roles. contractType = CDI|CDD|STAGE|ALTERNANCE|AUTRE. familySituation = CELIBATAIRE|MARIE|PACSE|DIVORCE|VEUF. hireDate/endDate as YYYY-MM-DD. referencePeriodStart as MM-DD (e.g. 06-01).')]
|
||||
#[McpTool(name: 'update-user', description: 'Update a user HR/profile fields (admin). Does NOT change password or roles. contractType = CDI|CDD|STAGE|ALTERNANCE|AUTRE. hireDate/endDate as YYYY-MM-DD. referencePeriodStart as MM-DD (e.g. 06-01).')]
|
||||
class UpdateUserTool
|
||||
{
|
||||
public function __construct(
|
||||
@@ -36,8 +35,6 @@ class UpdateUserTool
|
||||
?float $annualLeaveDays = null,
|
||||
?string $referencePeriodStart = null,
|
||||
?float $initialLeaveBalance = null,
|
||||
?string $familySituation = null,
|
||||
?int $nbChildren = null,
|
||||
): string {
|
||||
if (!$this->security->isGranted('ROLE_ADMIN')) {
|
||||
throw new AccessDeniedException('Access denied: ROLE_ADMIN required.');
|
||||
@@ -75,15 +72,6 @@ class UpdateUserTool
|
||||
if (null !== $initialLeaveBalance) {
|
||||
$user->setInitialLeaveBalance($initialLeaveBalance);
|
||||
}
|
||||
if (null !== $familySituation) {
|
||||
$user->setFamilySituation(
|
||||
FamilySituation::tryFrom($familySituation)
|
||||
?? throw new InvalidArgumentException(sprintf('Unknown family situation "%s".', $familySituation)),
|
||||
);
|
||||
}
|
||||
if (null !== $nbChildren) {
|
||||
$user->setNbChildren($nbChildren);
|
||||
}
|
||||
|
||||
$this->entityManager->flush();
|
||||
|
||||
|
||||
@@ -389,8 +389,6 @@ final class Serializer
|
||||
'annualLeaveDays' => $u->getAnnualLeaveDays(),
|
||||
'referencePeriodStart' => $u->getReferencePeriodStart(),
|
||||
'initialLeaveBalance' => $u->getInitialLeaveBalance(),
|
||||
'familySituation' => $u->getFamilySituation()?->value,
|
||||
'nbChildren' => $u->getNbChildren(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user