Files
Lesstime/docs/superpowers/specs/2026-05-22-absence-legal-compliance-review.md
T
Matthieu 4858f73d07 docs(absences) : aide /help du module + revue de conformité légale
- frontend/content/help/06-absences.md : nouveau chapitre d'aide « Absences »
  (poser une demande, lecture des soldes, mode de décompte des CP, ajout d'un
  salarié — nouveau ou déjà présent via le solde initial). Enregistré dans help.vue.
- revue de conformité (Syntec/RGPD) du module absences : rapport d'audit avec
  constats légaux et RGPD + recommandations.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 14:29:01 +02:00

14 KiB
Raw Blame History

Audit de conformité légale & RGPD — Module « Gestion des absences » (Lesstime)

Date : 2026-05-22 Périmètre : code backend (src/Enum, src/Entity, src/Service, src/State, src/Command, src/Controller/Absence), fixtures, migrations et front (frontend/components/absence, frontend/pages/absences.vue, team-absences.vue). Nature : audit documentaire. Aucune modification de code n'a été réalisée. Avertissement : ce document est une analyse technique de conformité, pas un avis juridique formel. Faire valider par un conseil RH/juridique avant mise en production paie.


1. Convention collective applicable

Constat

Le code (AbsencePolicy par défaut, fixtures AppFixtures.php) part d'hypothèses « Syntec » sans le formaliser : 25 jours ouvrés de CP, période de référence 1er juin → 31 mai (referencePeriodStart = '06-01'), décompte en jours ouvrés par défaut (countWorkingDaysOnly = true).

Convention probable

Pour une société d'édition de logiciels / services informatiques (« bureau informatique »), c'est très majoritairement la Convention collective nationale Syntec — IDCC 1486 (« Bureaux d'études techniques, cabinets d'ingénieurs-conseils et sociétés de conseils ») qui s'applique. C'est cohérent avec les valeurs codées (25 jours ouvrés, décompte en jours ouvrés).

⚠️ Ambiguïté à lever

La convention applicable ne se déduit pas de l'activité seule : elle dépend du code APE/NAF de l'entreprise et de l'activité principale réelle. Une SSII/éditeur peut relever de Syntec, mais certaines structures relèvent d'autres CCN. À confirmer sur le bulletin de paie ou auprès de l'expert-comptable. Le module code en dur des hypothèses Syntec mais ne stocke nulle part la CCN de référence : à documenter.

Sources


2. Conformité par type d'absence

Valeurs implémentées : voir AppFixtures.php (lignes 648-667) et User::$annualLeaveDays = 25.0.

Type Minimum légal / Syntec Implémenté Verdict
Congés payés (CP) Légal : 2,5 j ouvrables/mois = 30 j ouvrables/an (5 sem.). Syntec : 25 j ouvrés/an = équivalent 30 j ouvrables. Période réf. 01/06→31/05. daysPerYear = 25, décompte jours ouvrés, période 06-01, acquisition 1/12 par mois (AccrueLeaveCommand). Conforme sur le principe. ⚠️ Réserves : (a) pas de règle d'arrondi légal à l'entier supérieur (art. L3141-7) ; (b) pas de congés d'ancienneté Syntec (1 j à 5 ans, 2 j à 10 ans, 3 j à 15 ans, 4 j à 20 ans) ; (c) acquisition figée à annualLeaveDays/12 sans assimilation des périodes d'arrêt maladie (voir §3).
Mariage / PACS Légal : 4 jours minimum (L3142-4). Syntec : 4 jours ouvrés. daysPerEvent = 4, justificatif requis. Conforme.
Décès proche Légal : enfant = 5 j min (et jusqu'à 7 j + congé deuil 8 j si enfant <25 ans) ; conjoint/partenaire = 3 j ; père/mère/beau-parent/frère/sœur = 3 j (L3142-4). Syntec : barème ≥ légal, dont jusqu'à ~22 j pour décès enfant <25 ans. Un seul Bereavement à daysPerEvent = 3, sans distinction du lien de parenté. Non conforme. Un forfait unique de 3 j est inférieur au minimum légal pour le décès d'un enfant (5 j) et ignore le congé de deuil (8 j) et le barème Syntec. Dérive : sous-attribution de droits.
Naissance Légal : 3 jours min (en sus du congé paternité). Type absent de l'enum AbsenceType. Manquant. La naissance n'est pas gérée ; aucun congé naissance ni paternité.
Congé parental d'éducation Cadre L1225-47 s. : suspension du contrat, durée 1 an renouvelable 2×, jusqu'aux 3 ans de l'enfant, non rémunéré, ancienneté 1 an. ParentalLeave : daysPerYear/daysPerEvent null, decrementsBalance() = true (décrémente un solde). ⚠️ Modélisation incorrecte. Le congé parental est une suspension longue (mois/années), pas un décompte sur solde annuel. Le traiter comme une absence qui « décrémente un solde » (vide ici, donc 0) n'a pas de sens métier. À modéliser comme suspension, sans solde.
Arrêt maladie Indemnisé par la Sécu ; ne se déduit jamais des CP. Depuis loi 2024-364 du 22/04/2024 : acquiert 2 j ouvrables/mois de CP (maladie non pro). SickLeave : decrementsBalance() = false → ne touche aucun solde. Conforme sur le non-décompte. ⚠️ Manque : l'acquisition de CP pendant l'arrêt (2 j/mois) n'est pas implémentée — AccrueLeaveCommand crédite annualLeaveDays/12 quel que soit le statut, sans tenir compte des arrêts. Impact paie potentiel.

Décompte des jours (AbsenceDayCalculator)

  • Dimanche jamais compté ; samedi compté seulement en « jours ouvrables » ; jours fériés exclus ; demi-journées aux bornes à -0,5. Logique correcte.
  • PublicHolidayProvider : 11 jours fériés métropole + Pâques/Ascension/Pentecôte via Computus. Correct hors Alsace-Moselle (Vendredi saint + 26/12 non gérés — documenté comme hors périmètre, à signaler si salariés concernés).
  • ⚠️ La demi-journée à -0,5 est appliquée sans vérifier que la borne tombe sur un jour décompté : si startHalfDay est posé alors que le jour de début est un week-end/férié (non compté), on retire quand même 0,5, ce qui peut sous-décompter. Cas limite à border côté validation.

3. Constats RGPD / dérive

🔴 BLOQUANT — Fuite des données RH/familiales à tous les utilisateurs authentifiés

Fichiers : src/Entity/User.php (lignes 32-37, groupes user:list), config/packages/security.yaml (ligne 69).

Les opérations API GET /api/users (collection) et GET /api/users/{id} n'ont aucun attribut security : seule la règle globale ^/api → IS_AUTHENTICATED_FULLY s'applique. N'importe quel salarié (ROLE_USER) peut donc lister tous ses collègues et lire le groupe user:list, qui expose : familySituation, nbChildren, hireDate, endDate, contractType, workTimeRatio, annualLeaveDays, initialLeaveBalance et roles.

C'est une violation directe des principes RGPD de minimisation et de limitation d'accès (référentiel CNIL RH) : la situation familiale et le nombre d'enfants d'un collègue n'ont aucune raison d'être accessibles à un pair. Gravité maximale (donnée personnelle sensible au sens RH diffusée largement).

🟠 MOYEN — Collecte de familySituation / nbChildren non justifiée (non-minimisation)

Fichiers : User.php (119-125), EmployeeDrawer.vue, Serializer.php (MCP, 392-393).

Recherche d'usage exhaustive : ces deux champs sont stockés, saisis dans le formulaire RH et exposés (API + MCP), mais ne sont utilisés dans AUCUN calcul d'absence (ni AbsenceDayCalculator, ni AbsenceBalanceService, ni AccrueLeaveCommand, ni les policies). Or :

  • Le module gère le décès via un forfait unique sans lien de parenté → familySituation/nbChildren ne servent pas au congé décès.
  • La naissance et le congé parental ne sont pas calculés à partir de nbChildren.

Conséquence : collecte sans finalité opérationnelle = violation du principe de minimisation (art. 5.1.c RGPD). Soit ces champs servent réellement un calcul légal (alors les implémenter et le documenter dans le registre des traitements), soit ils doivent être supprimés. En l'état c'est une dérive de collecte. De plus la situation familiale peut révéler indirectement l'orientation/la vie privée → prudence renforcée.

🟠 MOYEN — Exposition de données RH via le MCP

Fichier : src/Mcp/Tool/Serializer.php (392-393). Le serializer MCP renvoie familySituation et nbChildren. Le serveur MCP HTTP pointe sur la PROD (cf. mémoire projet). Toute intégration MCP (assistant IA, scripts) peut donc aspirer ces données familiales. À restreindre / retirer du payload MCP.

🟡 MINEUR — Justificatif et motif : données potentiellement sensibles

Fichiers : AbsenceRequest::$reason, justificationFileName, AbsenceJustification*Controller.

  • Le contrôle d'accès au justificatif est correct (propriétaire ou admin uniquement, AbsenceJustificationDownloadController lignes 38-40).
  • Mais un justificatif d'arrêt maladie peut contenir des données de santé. Le champ reason (texte libre) peut aussi en contenir. Recommandations CNIL : ne stocker que le volet administratif, durée de conservation limitée, accès tracé. Aucune politique de rétention/purge n'est implémentée (fichiers et demandes conservés indéfiniment).

🟡 MINEUR — Approbation sans contrôle de solde

Fichier : AbsenceReviewProcessor.php. L'approbation déplace les jours de pending vers taken sans vérifier que le solde disponible est suffisant — un solde peut devenir négatif. Pas un problème RGPD mais une dérive de calcul (congés non acquis posables sans alerte). À noter : getAvailable() autorise volontairement de poser les CP « en cours d'acquisition » (N), ce qui est un choix d'entreprise admis mais à confirmer (certaines entreprises n'autorisent que le N-1).

Points conformes

  • Accès aux AbsenceRequest / AbsenceBalance : les providers (AbsenceRequestProvider, AbsenceBalanceProvider) filtrent bien par propriétaire pour un non-admin (un salarié ne voit que ses propres demandes/soldes). Le filtre user n'est appliqué que si ROLE_ADMIN.
  • Approbation/rejet/suppression réservés à ROLE_ADMIN.
  • Upload justificatif limité au propriétaire/admin, MIME validé côté serveur, taille plafonnée 10 Mo.

4. Recommandations actionnables

Priorité 1 — Bloquant (à corriger avant prod)

  1. Restreindre GET /api/users et GET /api/users/{id} : ajouter security: "is_granted('ROLE_ADMIN')" sur ces opérations, OU créer un groupe de sérialisation « annuaire » minimal (id, username, avatar) pour les non-admins et réserver user:list (champs RH) à l'admin via un contexte conditionné par le rôle. Retirer impérativement familySituation, nbChildren, hireDate, endDate, contractType, annualLeaveDays, initialLeaveBalance, roles de toute vue accessible à ROLE_USER.

Priorité 2 — Moyen

  1. Trancher sur familySituation / nbChildren : soit les supprimer (recommandé tant qu'aucun calcul ne les utilise), soit les rattacher à une finalité réelle (barème décès/naissance) et les inscrire au registre des traitements. Les retirer du payload MCP (Serializer.php).
  2. Refondre le congé décès : remplacer le forfait unique 3 j par un barème par lien de parenté conforme au minimum légal (enfant ≥ 5 j + deuil 8 j ; conjoint/parent/fratrie 3 j) et au barème Syntec applicable.
  3. Ajouter le congé naissance (3 j min) et, le cas échéant, le congé paternité.
  4. Remodéliser le congé parental comme suspension de contrat (sans décompte de solde annuel).

Priorité 3 — Mineur / robustesse

  1. Acquisition CP pendant arrêt maladie : aligner AccrueLeaveCommand sur la loi 2024-364 (2 j ouvrables/mois pour maladie non pro, plafond annuel).
  2. Congés d'ancienneté Syntec : implémenter le barème (1/2/3/4 j à 5/10/15/20 ans) et l'arrondi légal à l'entier supérieur.
  3. Politique de rétention : définir une durée de conservation des AbsenceRequest, reason et justificatifs (santé), avec purge automatique ; tracer les accès aux justificatifs.
  4. Contrôle de solde à l'approbation + garde-fou demi-journée sur jour non décompté dans AbsenceDayCalculator.
  5. Documenter la CCN de référence dans la config (ne pas la coder en dur implicitement) et confirmer Syntec via le code APE de l'entreprise.

Sources (règles légales citées)