﻿# Projet local

Stack :
- PHP 8.3.14
- Laravel
- MySQL 9.1
- Keycloak

# Règles générales

- Réponses en français
- Code complet uniquement
- Pas d'explications basiques
- Modifier uniquement ce qui est demandé
- Ne pas proposer de refactor inutile
- Commencer par lire le contenu de l'existant
- Modifier directement le code
- Tous les messages de confirmation doivent s'afficher dans des fenêtres modales dont la gestion est externalisée (principe DRY de Laravel)
- Aucune purge / suppression en masse (tables ACL, scans, données, etc.) sans autorisation explicite de l'utilisateur
- Interdiction de modifier les fichiers des autres applications sans autorisation explicite de l'utilisateur
- Interdiction de modifier les paramètres PHP et WAMP

# PHP

- Utiliser PHP 8+ strict
- PDO pour accès DB (si hors Laravel)
- Requêtes préparées obligatoires
- Pas de code obsolète (mysql_* interdit)

# Laravel

- Respecter conventions Laravel
- Utiliser Eloquent sauf demande contraire
- Utiliser migrations pour DB
- Utiliser services/controllers propres
- Pas de logique métier dans les controllers si possible

# MySQL

- SQL explicite et lisible
- Index si performance critique
- Pas de requêtes non paramétrées
- Respect schéma existant

# Keycloak

- Authentification par Keycloak via LoginLibrary
- Interdiction de modifier les fichiers de LoginLibrary
- Rôles renvoyé par Keycloak
- Groupes renvoyé par Keycloak

# Divers

- Messages de confirmation dans des fenêtres modales
- Messages d'alerte dans des fenêtres modales
- Fenêtres modales toutes sur le même modèle

# API / intégrations

- JSON standard
- REST privilégié
- Toujours gérer erreurs HTTP

# Sécurité

- Validation des inputs obligatoire
- Pas d'injection SQL
- Pas de secrets en dur dans le code

# Session IAM — Juillet 2026

## Architecture
- `personnes` = référentiel maître unique (pas de doublons), `sources` (JSON) liste les apps où la personne existe
- `comptes` → `application_id` (FK → `applications.id`)
- `applications` : GestUp, RH, NC, AD, Microsoft365, Keycloak
- `Compte::forApp('AD')` scope via `whereHas('application', fn => $q->where('code', ...))`

## Connecteurs
- Types disponibles : `AD`, `Microsoft365`, `IHTS`, `Keycloak`, `Partage` (seuls ceux avec classe implémentée)
- Type modifiable en édition (select caché + lien "Changer")
- Configuration en JSON, cast `EncryptedJson` (retourne `[]` si null, null si vide)
- Actions : Découvrir, Synchroniser, Importer, Réconcilier, Scan SP (M365)
- `ConnecteurController@executer` dispatch les scans longs en jobs asynchrones

## Scans asynchrones (SharePoint + Fichiers)

### SharePoint (`ScanSharePointJob`)
- Scanne récursivement un site SharePoint via Graph API
- Sauvegarde les ACL dossier par dossier (incrémental)
- Vérifie `estStoppe()` entre chaque dossier (statut `stopped` → arrêt propre)
- `stopped_le` enregistré si arrêté
- Utilise `Microsoft365Connector` avec méthodes publiques : `authenticate()`, `api()`, `getAll()`, `findSharePointSite()`, `getItemPermissions()`, `mapPermissionEntries()`

### Fichiers Partages (`ScanPartageJob`)
- Scanne via `icacls /T` (local → UNC → net use)
- Sauvegarde les ACL en une fois après parsing
- Vérifie `estStoppe()` avant de lancer icacls et après
- Replit la logique de `PartageConnector` (execIcacls, parseIcaclsOutput, parseAclEntry)

### Queue worker
- `QUEUE_CONNECTION=database`
- `runQueueWorker()` dans `ConnecteurController` : crée un .bat temporaire → `start /B ""` (évite PHP_BINARY = httpd.exe sous Apache)
- Fichier `.bat` dans `storage/app/queue-worker.bat`, log dans `storage/logs/queue-worker.log`

### Lancement des scans (process détaché)
- **NE JAMAIS utiliser `app()->terminating()`** — bouffe la RAM Apache (11 Go observé)
- Méthodes testées par ordre de priorité (dans `lancerScan()`):
  1. `PowerShell Start-Process` → `run-scan.bat` (prioritaire, pas de COM, process indépendant)
  2. WMI COM `Win32_Process.Create` → direct, propre
  3. `wmic process call create` → ligne de commande WMI
- **Piège WMI/wmic (fixé août 2026)** : `Win32_Process.Create` et `wmic` lancent le
  processus SANS passer par un shell → les redirections `>> "log" 2>&1` passées en
  arguments littéraux font échouer artisan (`Too many arguments to "scan:run"`), le
  scan reste `running` sans heartbeat et est flaggé « gelé ». Les deux méthodes
  utilisent donc `cmd.exe /c <chemin-run-scan.bat> <type> <scanId>` (run-scan.bat gère
  lui-même la redirection du log). Pas de guillemets `""...""` autour du bat (chemin
  sans espaces, le `""...""` casse le parsing wmic : « Format non valide »).
- Si tout échoue → exception (scan NON lancé, RAM Apache protégée)
- `storage/app/run-scan.bat` : batch permanent qui appelle `php artisan scan:run`
- Colonne `erreurs` des scans : type **json** (sharepoint_scans.erreurs était `text`
  64 Ko → « Data too long for column 'erreurs' » quand le scan remonte beaucoup
  d'erreurs, corrigé par migration `2026_08_14_000002_erreurs_json_sharepoint_scans`)
- Logs : `storage/logs/scan-{type}-{scanId}.log`

### Statuts des scans
- `pending` → `running` → `completed` / `failed` / `stopped` / `remplace`
- `stopping` n'existe plus (remplacé par `stopped` direct)
- Auto-refresh page toutes les 5s si scan en cours
- Un seul scan par type à la fois (vérifié dans ConnecteurController)
- Bouton Stop → `statut='stopped'`, `stopped_le=now()`

### Remplacement incrémental des ACL (Août 2026)
- Les entrées du **nouveau scan remplacent celles de l'ancien** (`chemin_complet`), mais **en différé**
  (`remplacerAnciensScans()` dans `ScanSharePointJob` / `ScanPartageJob`) :
  pendant le scan le job ne fait qu'INSCRIRE les nouvelles ACL ; ce n'est qu'à la fin, si le scan s'est
  terminé correctement (`completed`) ou a été arrêté proprement (`stopped`), que le **remplacement par
  UPSERT par ligne** a lieu sur les seuls chemins réellement couverts :
  - les entrées existantes des anciens scans (même `chemin_complet` + type + principal) sont **mises à
    jour (UPDATE)** avec les valeurs du scan courant, puis rattachées à ce scan ;
  - les nouvelles entrées sans équivalent ancien sont **conservées (INSERT)** ;
  - seules les entrées réellement **devenues obsolètes** (retirées côté source, sans équivalent dans le
    scan courant) sont supprimées.
  Aucun vidage possible : les ACL sont remplacées par mise à jour, pas par purge.
- Un scan qui **échoue (`failed`)** **garde ses entrées partielles** : `marquerEchec()` ne purge
  jamais les tables ACL, le scan reste visible dans la fusion d'affichage (statut `failed` ajouté à
  `STATUT_ACTIFS`). Les données partielles restent donc consultables jusqu'au prochain scan réussi.
- Sous Windows, **les ACL ne se gèrent pas au niveau des fichiers** : `ScanPartageJob` ignore les
  objets fichiers (et leurs ACE rattachées) — seuls les dossiers sont conservés.
- Seules les entrées qui **n'existent plus du tout côté source** (sans équivalent dans le scan courant)
  sont supprimées lors du remplacement.
- Un scan **arrêté** remplace aussi l'ancien, mais **sans supprimer ce qu'il n'a pas scanné** :
  dossiers non atteints → anciennes entrées conservées.
- Affichage = **fusion (union)** des scans actifs. Statuts actifs : `pending`, `running`, `completed`, `stopped`, `failed`
  (constante `STATUT_ACTIFS` sur les modèles ; `SharePointScan::idsActifs()` / `PartageScan::idsActifs()`).
- Un ancien scan dont toutes les entrées ont été remplacées passe en `remplace` (visible grisé/barré sur l'index,
  non inclus dans la fusion) via `marquerRemplace()` appelé en fin de job et sur Stop (`PermissionsController`).
- `PermissionsController::listeDossiers()` prend désormais `array $scanIds` (`whereIn`) ; le cache 60 s est
  désactivé tant qu'un scan (`pending`/`running`) est en cours pour laisser voir les ACL en direct.
- `RunScan` re-lit le scan via `find()` (et non `refresh()`) après l'exécution : il ne lève plus d'exception
  si le scan a été supprimé en cours de route.

### Comptage réel des fichiers (Août 2026)
- Les fichiers à ACL identique à leur dossier parent ne sont pas stockés dans les ACL :
  le compteur "· X fichiers" de l'arbre serait faux. Deux nouvelles tables portent le
  **nombre réel de fichiers par dossier** : `sharepoint_dossiers` et `partage_dossiers`
  (`scan_id` + `chemin_complet` unique + `nb_fichiers`).
- `ScanSharePointJob` compte les fichiers à chaque liste de dossier (même sans ACL
  dérogatoire) et sauvegarde le compteur par `updateOrCreate` pendant le scan ;
  `ScanPartageJob` compte les fichiers détectés par `estFichierAbsolu` pendant le
  parsing icacls (map parent → nb) et les sauvegarde en `upsert` **incrémental**
  (flush idempotent à chaque changement de dossier parent + heartbeat 15 s), puis
  en une passe aux sorties du scan (fin/stop).
- Remplacement différé identique aux ACL : `remplacerAnciensCompteurs()` en fin de scan
  (completed/stopped) reprend (UPDATE/INSERT) les compteurs des dossiers réellement
  couverts, sans purge ; les dossiers non atteints gardent les compteurs des anciens scans.
- `PermissionsController::arbre()` lit les compteurs réels (repli sur le comptage ACL
  si compteurs absents ou si filtre principal actif). Pendant un scan, ancien et nouveau
  scan ont un compteur pour le même chemin : un `ROW_NUMBER()` par chemin ne conserve
  que le compteur du scan le plus récent (pas de double comptage).
- `relancerSharepoint`/`relancerPartage` suppriment aussi les compteurs du scan relancé.

### Tables
- `sharepoint_scans` : id, statut, site_url, site_name, scanne_le, stopped_le, total_dossiers, total_entrees_acl, erreurs, declenche_par
- `sharepoint_acl_entries` : id, sharepoint_scan_id, dossier, chemin_complet, type, principal, email, droits, heritage, est_herite
- `sharepoint_dossiers` : id, sharepoint_scan_id, chemin_complet (unique avec scan), nb_fichiers
- `partage_scans` : id, statut, serveur, chemin, scanne_le, stopped_le, total_dossiers, total_entrees_acl, erreurs, declenche_par
- `partage_acl_entries` : id, partage_scan_id, dossier, chemin_complet, type, principal, sid, droits, heritage, est_herite
- `partage_dossiers` : id, partage_scan_id, chemin_complet (unique avec scan), nb_fichiers

## Module Changements
- Supprimé (approbation des modifications de connecteurs retirée)

## Fichiers clés récents
- `app/Jobs/ScanSharePointJob.php` — Job async scan SP
- `app/Jobs/ScanPartageJob.php` — Job async scan fichiers
- `app/Http/Controllers/PermissionsController.php` — stopPartage, stopSharepoint
- `app/Modules/Connecteurs/Controllers/ConnecteurController.php` — executer() avec dispatch async, runQueueWorker()
- `app/Modules/Connecteurs/Connectors/Microsoft365Connector.php` — méthodes passées en public pour les jobs
- `app/Http/Controllers/PermissionsController.php` — stop methods
- `resources/views/permissions/index.blade.php` — statuts + stop buttons + auto-refresh
- `resources/views/permissions/detail.blade.php` — arbre ACL dépliable
- `resources/views/modules/connecteurs/index.blade.php` — boutons désactivés si scan en cours
- `resources/views/modules/connecteurs/form.blade.php` — type éditable + aides config

## Pistes futures
- Réconciliation AD/M365 intégrée dans `iam:m365-sync` (étape 2)
- Keycloak : SSL certificate issue (cURL error 60)
- GroupeService : migration `source` → `application` pour le filtre dans l'UI
- Planifier scans automatiques (cron fichiers + commande artisan)
