---
suivi: 1233
date: 2026-08-07
sujet: Diag bascule groupes M365 GS_* → GSM_* (mail-enabled)
chantier: droits / m365 / entra
type: diagnostic
statut: poussé
hash: bbf2ae37
fichiers:
  - tools/diag/diag_groupes_m365_1233.php
  - docs/suivi/SUIVI_1233_diag_groupes_m365.md
---

# SUIVI #1233 — PHASE 1 DIAGNOSTIC — groupes M365

## PROMPT ENVOYÉ

Cartographie avant bascule éventuelle GS_* → GSM_* (groupes de sécurité avec
extension mail). Lecture seule. Script diag + fiche uniquement. Aucun code
métier, migration, Vue, rebuild, appel réseau, écriture Entra.

## FICHES LUES

- `#1232` / `#1230` — contexte SharePoint ; hors périmètre droits.
- `#571` (référencé dans code) — ingestion SSO / fallback `config/erp.php`.
- Discipline enseignes (`aModule`) : **non tenue** pour les groupes — nombreux
  `GS_*` en dur (routes, policies, services).

## OBJET 1 — Ce que l’ERP stocke (décision)

### Table `groupes_m365` (migration `2026_07_06_170000`)

| Colonne | Type | Rôle |
|---------|------|------|
| `id` | bigint PK | |
| `azure_object_id` | uuid nullable **unique** | GUID Entra **optionnel** |
| `nom` | string(150) **unique** | displayName reconnu |
| `libelle` | string nullable | |
| `actif` | bool | |
| `protege` | bool | empêche désactivation DSI/ADMIN |
| `est_prefixe` | bool | règle préfixe (ex. `ERP-`) |
| `prefixe_valeur` | string nullable | |
| timestamps | | |

Seeder explicite : `GS_DSI`, `GS_ADMIN_ERP` (protégés), `GS_DIRECTION_GENERALE`,
`GS_ENVOL`, `GS_HECTARE`, `GS_COMPTABILITE`, `GS_COMMUNICATION`, `GS_DAF`,
`GS_JURIDIQUE` + ligne préfixe `[prefixe:ERP-]`.

**Remplissage `azure_object_id`** : à mesurer en prod (script §2). Le seeder
**ne le renseigne pas**. L’admin `GroupesM365Controller` peut le lier.

### `users.m365_groups`

- Colonne **JSON** (cast Eloquent `array`).
- Contenu : **liste de noms** `["GS_DSI","GS_ENVOL",…]` — **pas de GUID**.
- Rempli au **login SSO** (`MicrosoftAuthController`) et resync admin
  (`AdminController::resyncUserM365Groups`) à partir des **displayName**
  `transitiveMemberOf`.

### Verdict autorité

**L’ERP identifie les groupes par NOM (displayName).**

- Un renommage Entra `GS_ENVOL` → `GSM_ENVOL` = remplacement de chaînes partout
  + mise à jour `groupes_m365.nom` + re-login / resync des `users.m365_groups`.
- Recréer un groupe **sous le même nom** : droits OK (GUID change, mais non
  utilisé pour l’authz).
- `azure_object_id` coexiste pour l’admin de liaison, **ne fait pas autorité**
  dans `hasM365Group` / middleware / `whereJsonContains('m365_groups', 'GS_…')`.

Volumes exacts (échantillon, fill rate GUID, users/groupe) → **script OVH**.

## OBJET 2 — Inventaire occurrences `GS_*`

### Décompte approx. (sources, hors `public/build` / `_tmp`)

Mesure locale `rg` (matches ≈ lignes contenant le code) :

| Code | ≈ matches (app/config/routes/db/js/tests/tools) | ≈ hors journal+tests (sécurité/UI) |
|------|--------------------------------------------------|-------------------------------------|
| `GS_DSI` | 602 | 213 |
| `GS_ADMIN_ERP` | 451 | 182 |
| `GS_COMPTABILITE` | 453 | 117 |
| `GS_DIRECTION_GENERALE` | 286 | 88 |
| `GS_ENVOL` | 296 | 41 |
| `GS_HECTARE` | 149 | 16 |
| `GS_JURIDIQUE` | 49 | 24 |
| `GS_COMMUNICATION` | 26 | 11 |
| `GS_GROUPE` | 25 | 4 |
| `GS_DAF` | 14 | 13 |
| `GS_GEMME` | 11 | 3 |
| `GS_PDG_ENVOL` | 1 | 1 |
| **Total ordre** | **~2360** | **~710** |

Une large part du total = `database/data/journal_mises_a_jour_*.php`
(`public_groupes`) + tests.

### Fichiers les plus concernés (sécurité)

- `routes/web.php` — **~30** middlewares `m365.groups:GS_…`
- `config/feature_permissions.php` — mapping droits par groupe
- `config/erp.php` — liste reconnue (fallback)
- `app/Models/User.php` — `isAdminERP()` hardcode `GS_ADMIN_ERP`
- `app/Http/Middleware/EnsureM365GroupMembership.php`
- Policies / services : `AppelOffrePolicy`, `AlerteService`, `RecordAcl`,
  `HasRecordHabilitations`, dashboard, juridique, dépôt factures, assistant…
- Front : `AppShell.vue`, `DepotFactureSaisieForm.vue`, pages Admin…

### Discipline « pas de code en dur »

**Non tenue** pour les groupes (contrairement aux enseignes / `aModule`).
Tout renommage = chantier de remplacement de chaînes + config + journal + tests.

### Codes hors seeder mais présents dans le code

| Code | Usage | Risque |
|------|--------|--------|
| `GS_GROUPE` | middleware Aktor (`routes/web.php`) | doit être dans `groupes_m365` sinon jamais ingéré au login |
| `GS_GEMME` | `ProgrammeEnseigneComfortFiltre` | confort UI ; pas dans seeder/config erp |
| `GS_PDG_ENVOL` | `HandleInertiaRequests` | 1 occurrence |

## OBJET 3 — Filtre sync appartenances

### Login / resync (celui qui peupple `users.m365_groups`)

`Microsoft365GroupService::TRANSITIVE_MEMBER_OF_GROUPS_URL` =

`/me/transitiveMemberOf/microsoft.graph.group?$select=displayName`

- **Pas** de `$filter` `securityEnabled` / `mailEnabled`.
- **Transitif** (imbriqué OK) — explicitement documenté vs `/me/memberOf`
  (`MicrosoftAuthController`).
- Puis filtre applicatif : displayName ∈ `groupes_m365.nom` actifs ou préfixe
  `ERP-` (`GroupeM365RecognitionService`).

→ Un groupe **mail-enabled + securityEnabled** remonte **oui**, s’il a le bon
`displayName` reconnu.

### Liste admin (`GraphApiService::listSecurityGroups`)

`$filter=securityEnabled eq true` — inclut les groupes de sécurité mail-enabled
(ils restent `securityEnabled=true`). Exclut les listes de distribution pures
(`securityEnabled=false`).

### Déclenchement

- Login web SSO (PHP-FPM) — principal.
- Resync admin web.
- **Pas** de sync CLI pour les appartenances user (HTTPS sortant CLI bloqué OVH).

## OBJET 4 — Diffusion / notifications

- Droits ERP : **uniquement** groupes de sécurité reconnus (noms `GS_*` / `ERP-`),
  pas un catalogue de listes de diffusion.
- Notifications digest Tasks : **par utilisateur** (Teams), pas vers une adresse
  de groupe (`TachesDigestCommand` / middleware web).
- Adresses mail hardcodées ailleurs (ex. leads marketing
  `LeadMarketingNotificationEmailBuilder`) — **pas** liées aux `GS_*`.
- Expéditeur Azure : clé `AZURE_MAIL_EXPEDITEUR` dans `config/services.php`
  (ne pas afficher `.env`).
- Fallback assistant DSI : `ASSISTANT_DSI_PERSONNES_IDS` (IDs user, pas groupe).

**Effet de bord fusion** : si demain une notif partait vers le mail d’un groupe
de diffusion distinct du GS_*, fusionner les objets Entra changerait la boîte ;
**aujourd’hui le code des droits ne cible pas ces boîtes**.

## OBJET 5 — Stratégie de bascule (écrite, non exécutée)

Recommandation : **cohabitation** temporaire.

1. Côté Entra (Robin) : créer `GSM_*` mail-enabled **ou** (préféré objet 6)
   activer l’extension mail sur le `GS_*` existant sans renommer.
2. Si renommage : ajouter `GSM_*` dans `groupes_m365` **en plus** des `GS_*` ;
   faire coexister les deux dans la reconnaissance ; users peuvent avoir les deux
   pendant la fenêtre.
3. **Témoin** : `GS_GEMME` ou `GS_HECTARE` — **jamais** `GS_ADMIN_ERP` / `GS_DSI`
   en premier.
4. Vérifier : user témoin a le nouveau nom dans `m365_groups` après **re-login**
   (ou resync admin) **avant** de retirer l’ancien groupe Entra / l’ancienne ligne.
5. Retour arrière : réactiver l’ancien groupe Entra + remettre `nom` / config ;
   user → resync ou re-login. Délai = immédiat après resync web.
6. ERP vs Microsoft : listes distinctes (fiche synthèse).
7. Rafraîchissement : **pas de cron d’appartenances** — visible au prochain login
   ou resync manuel admin.

## OBJET 6 — Faut-il vraiment renommer ?

| Option | Coût ERP | Coût Entra | Atteint l’unification mail+droits ? |
|--------|----------|------------|--------------------------------------|
| **A — Extension mail sur `GS_*` existants** (garder le nom) | **~0** ligne code | Changer la nature du groupe (GUID conservé) | Oui |
| **B — Renommer en `GSM_*`** | **~700+** occurrences sécurité/UI + journal (~1600+) + tests + reseeding `groupes_m365` + resync tous users | Renommer ou recréer | Oui + convention de nom |

**Le diag recommande d’arbitrer vers A** sauf contrainte métier forte sur le
préfixe `GSM_`. Le renommage est un chantier de sécurité, pas un réglage.

## SCRIPT

```text
php tools/diag/diag_groupes_m365_1233.php
```

OVH uniquement pour volumes réels. Local MySQL peut être down (ne pas confondre).

## DÉPLOIEMENT

```text
git pull origin main
```

Pas de migrate / journal / build. Lancer le script, coller la sortie.

## LEÇON

Autorité = **nom** stocké en JSON. GUID table = optionnel admin. Filtre
appartenances = transitif sans exclusion mailEnabled. Renommer = N hardcodes ;
étendre mail sans renommer = coût quasi nul côté ERP.
