Architectures IA locales pour PME suisses : RTX Spark, DGX Spark, RTX PRO 6000, H100. TCO réel, ingénierie RAG, conformité nLPD. Procédures opérationnelles publiées progressivement.
View the Project on GitHub doit4everyone/ia-locale-pme-suisse
| Retour au sommaire | Section précédente : §12 Synthèse des réunions Teams | Section suivante : §14 Documents protégés par Purview |
Statut : validé en lab sur VM-RAG-LAB, septembre 2026, sur un tenant Microsoft 365 synchronisé par Entra Connect : cinq sites, trois comptes de test, 18 réponses sur 18 conformes à la matrice de cloisonnement.
Ce que cette section documente : les documents des bibliothèques SharePoint Online sont indexés dans la même collection que ceux du partage SMB, et chaque chunk porte les permissions SharePoint du fichier, traduites en identifiants Entra. Un utilisateur ne peut interroger que les documents qu’il peut ouvrir dans SharePoint. L’application d’indexation n’a accès qu’aux sites qui lui sont accordés explicitement, en lecture seule.
n8n (toutes les heures) → POST /admin/sync (rag-api)
1. indexer.py partage SMB
2. acl_resolver.py ACL NTFS
3. sp_indexer.py SharePoint Online (si SP_SITES est renseigné)
├→ fichiers, permissions, contenu (Graph, RAG-SharePoint-Indexer)
├→ groupes SharePoint du site (API REST SharePoint, même application)
├→ UPN, groupes, propriétaires (Graph, RAG-Identity-Resolver, §11)
└→ chunks avec autorises[] (Qdrant, collection documents)
→ reconstruction de l'index BM25
| Application | Permissions | Rôle |
|---|---|---|
RAG-SharePoint-Indexer (nouvelle) |
Sites.Selected (Graph et SharePoint), rôle read accordé site par site |
Lire les fichiers, leurs permissions et les groupes SharePoint des sites accordés |
RAG-Identity-Resolver (§11) |
User.Read.All, GroupMember.Read.All |
Traduire les UPN en identifiants, lire les propriétaires des groupes, vérifier l’existence des groupes |
L’application d’indexation ne sait rien de l’annuaire, et l’application d’identité ne voit aucun document. Chacune a le strict nécessaire.
sp_indexer.py réutilise l’extraction, le découpage et les embeddings d’indexer.py : un document SharePoint est traité exactement comme un document du partage.Avant toute ligne de code, inventorier ce que l’indexeur verra : c’est ce qui a fixé les règles de traduction de §13.4, et révélé plusieurs pièges.
Trois scripts PowerShell en lecture seule, dans scripts/sharepoint/, exécutés avec un compte administrateur :
| Script | Module | Produit |
|---|---|---|
Inventaire-Permissions-SharePoint.ps1 |
Microsoft.Graph.Authentication |
Permissions de chaque fichier et dossier, telles que Graph les expose (CSV) |
Inventaire-GroupesSharePoint.ps1 |
Microsoft.Online.SharePoint.PowerShell |
Composition des groupes SharePoint de chaque site (CSV) |
Resoudre-Identifiants.ps1 |
Microsoft.Graph.Authentication |
Nom et type des identifiants (GUID) trouvés dans les inventaires |
.\Inventaire-Permissions-SharePoint.ps1 `
-Sites "https://<tenant>.sharepoint.com/sites/RH","https://<tenant>.sharepoint.com/" `
-Sortie "C:\Temp\permissions-sharepoint.csv"
Deux pièges d’exécution.
- Ne jamais charger les modules SharePoint Online et Graph dans la même session PowerShell. Ils embarquent des versions différentes des mêmes bibliothèques d’authentification : le second module chargé échoue avec une erreur
TypeLoadExceptionpeu explicite. Une console par module.Connect-SPOServiceexige l’authentification moderne. Sans-ModernAuth $true, le module tente l’authentification héritée, refusée par les tenants actuels, et affiche seulement « Could not connect to SharePoint Online ». Le script la demande explicitement.
RAG-SharePoint-IndexerCertificat, sur la VM :
cd /etc/rag-certs
sudo openssl req -x509 -newkey rsa:2048 -nodes \
-keyout rag-sharepoint.key -out rag-sharepoint.crt \
-days 730 -subj "/CN=RAG-SharePoint-Indexer"
sudo chmod 600 rag-sharepoint.key
sudo cat rag-sharepoint.crt
openssl x509 -in rag-sharepoint.crt -noout -fingerprint -sha1 | tr -d ':' | cut -d= -f2
Dans le centre d’administration Entra :
RAG-SharePoint-Indexer, comptes de cet annuaire uniquement, sans URI de redirection.rag-sharepoint.crt, vérifier l’empreinte.Sites.Selected : fichiers, permissions des fichiers, contenu ;Sites.Selected : composition des groupes SharePoint, que Graph n’expose pas.User.Read ajoutée par défaut.
Sites.Selectedne donne accès à rien tant qu’un accès n’est pas accordé site par site. C’est la différence essentielle avecSites.Read.All, qui ouvre tous les sites du tenant, y compris ceux créés plus tard.
Accorder-SitesSelected.ps1 accorde le rôle read à l’application sur une liste de sites, via Graph, puis affiche les accès applicatifs de chaque site. Il demande la portée déléguée Sites.FullControl.All, nécessaire pour gérer les accès des applications : l’application, elle, ne reçoit que la lecture.
.\Accorder-SitesSelected.ps1 `
-AppId "<ID-APPLICATION>" -AppNom "RAG-SharePoint-Indexer" `
-Sites "https://<tenant>.sharepoint.com/sites/RH","https://<tenant>.sharepoint.com/"
# Consultation seule des accès applicatifs d'un site
.\Accorder-SitesSelected.ps1 -AppId "<ID-APPLICATION>" -AppNom "RAG-SharePoint-Indexer" `
-Sites "https://<tenant>.sharepoint.com/sites/RH" -Lister
L’affichage des accès existants a une utilité propre : toute autre application qui apparaît sur un site est à examiner.
sonde_sharepoint.py vérifie ce que l’application peut réellement lire, avant d’écrire l’indexeur. À exécuter dans le conteneur rag-api :
docker exec -e PYTHONPATH=/app -w /app rag-api python3 /rag-pipeline/sonde_sharepoint.py \
--client-id <ID-APPLICATION> --thumbprint <EMPREINTE> \
--site https://<tenant>.sharepoint.com/sites/RH
Validé en lab, septembre 2026 :
| Moment | Résultat |
|---|---|
| Avant l’accord | Jeton valide, mais 403 sur chaque site : la permission seule ne donne rien |
| Après l’accord, rôle read | Sites, bibliothèques, fichiers, permissions de chaque fichier et contenu lisibles |
| Groupes SharePoint | Propriétaires, Membres et Visiteurs lisibles ; seuls les groupes système (« Limited Access », « SharingLinks ») sont refusés, parce que leurs membres ne sont visibles que par eux-mêmes |
| Chiffrement | Document de test : format Office ouvert ; contrat étiqueté : conteneur OLE, donc chiffré |
Les groupes système refusés ne manquent pas à l’indexeur : les groupes « Limited Access » ne donnent pas accès au contenu, et les destinataires des liens de partage sont déjà fournis par Graph dans les permissions de chaque fichier. Aucun droit supplémentaire n’est nécessaire.
| Membre trouvé | Format dans SharePoint | Traduction dans autorises[] |
|---|---|---|
| Utilisateur | i:0#.f\|membership\|<UPN> ou identité Graph |
entra:usr:<id> |
| Groupe de sécurité (cloud ou synchronisé AD) | c:0t.c\|tenant\|<id> |
entra:grp:<id>, si le groupe existe |
| Membres d’un groupe Microsoft 365 | c:0o.c\|federateddirectoryclaimprovider\|<id> |
entra:grp:<id> |
| Propriétaires d’un groupe Microsoft 365 | même format, suffixé _o |
entra:usr:<id> de chaque propriétaire |
| Tous les utilisateurs internes | c:0-.f\|rolemanager\|spo-grid-all-users/<tenant> |
entra:tous-internes |
| Groupe SharePoint | identifiant du groupe dans le site | traduction de chacun de ses membres |
| Lien « personnes précises » | lien de portée users |
les personnes désignées |
| Lien « toute l’organisation » ou anonyme | lien de portée organization ou anonymous |
ignoré |
| Invité externe | UPN contenant #ext# |
ignoré |
| Compte système, groupes « Limited Access » | SHAREPOINT\system |
ignoré |
| Rôle d’annuaire, groupe supprimé | identifiant introuvable | ignoré, signalé dans le rapport |
SharePoint n’a pas de refus explicite : interdits[] reste vide.
Graph présente les propriétaires d’un groupe Microsoft 365 comme le groupe lui-même, avec le rôle owner. Le traduire en entra:grp:<id> donnerait l’accès à tous les membres du groupe, y compris sur un document réservé aux propriétaires. L’indexeur reconnaît ce cas et lit la liste des propriétaires : chacun reçoit un entra:usr:<id>.
Dans SharePoint, un lien de portée organisation permet à tout employé qui possède le lien d’ouvrir le document. Le RAG ne peut pas savoir qui le possède : il traite le document comme non partagé par ce lien. C’est la seule divergence avec SharePoint, et elle est volontairement plus restrictive.
auth.py ajoute entra:tous-internes aux comptes membres du tenant, pas aux invités. Validé en lab avec un compte sans licence Microsoft 365 : il accède au site ouvert à tous, et à rien d’autre.
Qdrant compare les valeurs de autorises[] en respectant la casse. auth.py et sp_indexer.py écrivent tous les identifiants en minuscules : une traduction en majuscules d’un côté seulement rendrait les documents invisibles, sans erreur.
Un document protégé par une étiquette avec chiffrement n’est pas un fichier Office ouvert, mais un conteneur OLE chiffré. L’indexeur le détecte, ne l’indexe pas, retire ses éventuels anciens chunks, et le compte dans le rapport (« chiffré, non indexé »). Rien n’est indexé à moitié. Le déchiffrement fera l’objet de la section Purview : il devra respecter à la fois les permissions SharePoint et les droits de l’étiquette.
Le chiffrement protège un fichier, pas son contenu une fois copié. Dans le lab, les contrats de travail originaux étaient chiffrés, mais le site RH contenait des copies en clair : exports PDF et copies Word. Ces copies sont indexées, visibles des seuls membres du site RH, conformément aux droits SharePoint. Mais un outil d’IA rend ce genre de copie beaucoup plus facile à trouver que ne le fait une arborescence de dossiers. Un inventaire du contenu indexé, avant la mise en service, permet de les repérer.
| Fichier | Rôle |
|---|---|
sp_indexer.py |
Indexation SharePoint, traduction des permissions, détection du chiffrement |
api/anon_auth.py |
Identifiant entra:tous-internes |
api/anon_main.py |
Étape SharePoint dans /admin/sync, citations SharePoint, fenêtres de contexte explicites |
scripts/sharepoint/ |
Inventaires, accord site par site, sonde |
En plus des champs d’indexer.py :
{
"source": "SharePoint/RH/Documents/Politique_RH.docx",
"source_type": "sharepoint",
"sp_site": "https://<tenant>.sharepoint.com/sites/RH",
"sp_drive": "Documents",
"sp_item_id": "01ABC…",
"sp_ctag": "\"c:{…},2\"",
"web_url": "https://<tenant>.sharepoint.com/…",
"autorises": ["entra:grp:<id>", "entra:usr:<id>"],
"interdits": []
}
source_type protège ces chunks du nettoyage des orphelins d’acl_resolver.py, qui ne concerne que le SMB (§11).
cTag, mêmes paramètres de découpage) : le fichier n’est pas retéléchargé. Seules ses permissions sont recalculées, et mises à jour si elles ont changé.main.py enrichit les citations selon la source : chemin UNC pour le SMB, et pour SharePoint un lien cliquable vers le document :
[Politique_RH.docx : SharePoint, site RH, Documents](https://<tenant>.sharepoint.com/…)
SharePoint vérifie lui-même les droits de celui qui clique.
SP_CLIENT_ID=<ID-APPLICATION-SHAREPOINT>
SP_CERT_THUMBPRINT=<EMPREINTE-CERTIFICAT-SHAREPOINT>
SP_KEY_PATH=/etc/rag-certs/rag-sharepoint.key
# URL complètes, séparées par des virgules ; vide = étape désactivée
SP_SITES=https://<tenant>.sharepoint.com/sites/RH,https://<tenant>.sharepoint.com/
SP_EXCLUDE_DRIVES=Preservation Hold Library
# Au-delà, fichier non indexé ; déjà indexé, ses extraits sont supprimés (2.21.6)
SP_MAX_FILE_MB=50
SYNC_TIMEOUT_SHAREPOINT=900
LLM_NUM_CTX=16384
JUDGE_NUM_CTX=8192
SP_SITES doit contenir exactement les sites accordés en §13.3.2. Un site listé mais non accordé est signalé en erreur dans le rapport ; un site accordé mais non listé n’est pas indexé.
# Scripts : pas de reconstruction, le dossier est monté dans le conteneur
sudo cp /home/<utilisateur>/sp_indexer.py /home/<utilisateur>/sharepoint/sonde_sharepoint.py /root/rag-pipeline/
# Variables transmises au conteneur
cd /root/rag-stack
cp docker-compose.yml docker-compose.yml.bak
grep -q 'SP_CLIENT_ID=' docker-compose.yml || for v in JUDGE_NUM_CTX LLM_NUM_CTX SYNC_TIMEOUT_SHAREPOINT SP_MAX_FILE_MB SP_EXCLUDE_DRIVES SP_SITES SP_KEY_PATH SP_CERT_THUMBPRINT SP_CLIENT_ID; do
sed -i "/- TEAMS_STATE_FILE=\${TEAMS_STATE_FILE}/a\ - $v=\${$v}" docker-compose.yml
done
docker compose config --quiet && echo "Compose OK"
# main.py et auth.py : reconstruction
sudo cp /home/<utilisateur>/main.py /home/<utilisateur>/auth.py /root/rag-stack/api/
docker compose build --no-cache rag-api
docker compose up -d rag-api
docker exec rag-api printenv SP_SITES
Dans n8n, workflow de synchronisation du corpus, nœud POST /admin/sync : délai d’attente porté à 1800000 ms (30 minutes). La version publiée du workflow contient déjà cette valeur.
Premier passage manuel, en simulation (rien n’est écrit) :
docker exec -e PYTHONPATH=/app -w /app rag-api python3 /rag-pipeline/sp_indexer.py --dry-run
Le rapport (/var/log/rag/rapport_sharepoint_*.json) donne, pour chaque fichier, son statut et ses autorises[] traduits : c’est le moment de les comparer à la matrice de test, avant toute écriture.
/admin/sync reconstruit maintenant l’index BM25 après chaque synchronisation, même partiellement en échec : une étape peut avoir modifié Qdrant avant l’échec d’une autre, et l’index doit refléter son contenu. Une indexation lancée à la main, en dehors de /admin/sync, ne le reconstruit pas : redémarrer rag-api ou lancer une synchronisation.
| Élément | Rôle dans le test |
|---|---|
test-compta, compte AD synchronisé, membre du seul groupe AD Comptabilité, licencié |
Accès par groupe AD synchronisé, partage direct, refus |
test-client, compte AD synchronisé, sans licence, sans droit SharePoint |
« Tous les internes » uniquement |
| Compte administrateur | Accès à tous les sites |
Test_RAG_Finances (site Finances, sans partage) |
Accès par groupe AD |
Test_RAG_RH (site RH, sans partage) |
Cloisonnement |
Test_RAG_RH_Lien_Organisation (lien « toute l’organisation ») |
Règle de refus du lien |
Test_RAG_RH_Partage_Direct (partagé à test-compta) |
Permission lue fichier par fichier |
Chaque document de test contient une phrase avec un code unique (par exemple « Le code de test RH est RH-3159. ») : la réponse est soit le bon code, soit « information non disponible », sans ambiguïté possible.
Créer les comptes de test dans l’AD, pas dans Entra. Open WebUI authentifie les utilisateurs en LDAP sur l’AD : un compte créé uniquement dans Entra ne pourrait pas interroger le RAG. Pour un compte synchronisé, renseigner le lieu d’utilisation avant d’attribuer une licence : sans lui, l’attribution est refusée.
Étiquetage obligatoire. Si la stratégie d’étiquetage impose une étiquette, choisir une étiquette sans chiffrement pour les documents de test, n’y mettre aucune donnée qui déclencherait l’étiquetage automatique (numéro AVS, IBAN, terme médical), et vérifier l’étiquette le lendemain : l’étiquetage automatique côté serveur est asynchrone.
Validé en lab, septembre 2026 : 5 sites, 36 fichiers indexés, 10 chiffrés et non indexés, 2 fichiers Excel ignorés, 2 identifiants introuvables (le groupe supprimé et le rôle d’annuaire). Permissions traduites des documents de test toutes conformes avant écriture. Second passage : les 36 fichiers « inchangés », sans téléchargement. Intégration à /admin/sync : les trois étapes enchaînées, index BM25 reconstruit (325 chunks dans documents, dont 250 issus de SharePoint).
| Question | Administrateur | test-compta | test-client |
|---|---|---|---|
| Code de test Finances | FIN ✓ | FIN ✓ | Refusé ✓ |
| Code de test RH | RH ✓ | Refusé ✓ | Refusé ✓ |
| Code de test lien organisation | ORG ✓ | Refusé ✓ | Refusé ✓ |
| Code de test partage direct | DIR ✓ | DIR ✓ | Refusé ✓ |
| Guide de dépannage (site ouvert à tous) | Réponse ✓ | Réponse ✓ | Réponse ✓ |
| Jours de vacances, politique RH (document réel) | 25 / 27 jours ✓ | Refusé ✓ | Refusé ✓ |
18 réponses sur 18 conformes. Chaque règle de §13.4 est démontrée par un cas : groupe AD synchronisé, permission lue fichier par fichier, refus du lien organisation, « tous les internes » sans licence, cloisonnement sur un document réel.
Le guide de dépannage existe en deux exemplaires, sur un site réservé au service informatique et sur le site ouvert à tous. L’administrateur obtient la copie du site informatique, les deux autres comptes la copie du site ouvert, la seule à laquelle ils ont accès : le RAG ne se contente pas de refuser, il trouve la version autorisée du même contenu.
Questions générales. Pendant les tests, « Que dit la politique RH ? » n’a pas trouvé le document, alors que « Combien de jours de vacances prévoit la politique RH ? » l’a trouvé. Le premier diagnostic mettait en cause le découpage : les chunks commencent par leur titre de section (« Temps de travail et congés »), et le titre du document n’apparaît que dans le premier. Mais ce test a eu lieu pendant la panne décrite en §13.8.3 : la copie SMB du même document était alors invisible pour tous. Une fois la lecture du partage rétablie, la même question générale a obtenu une réponse complète et correctement citée. L’effet du titre absent des chunks n’est donc pas démontré : il sera mesuré au §10, en répétant le titre du document en tête de chaque chunk et en comparant les réponses sur un même jeu de questions.
Formats. Les fichiers Excel ne sont pas indexés, comme pour le SMB.
Liens organisation. Un document partagé uniquement par ce type de lien reste invisible pour ceux qui ont reçu le lien (§13.4.3).
Invités externes. Ils ne peuvent pas se connecter à Open WebUI (authentification LDAP sur l’AD) : leurs accès SharePoint ne sont pas traduits.
Documents chiffrés. Non indexés dans cette section (§13.5).
main.py ne demandait pas de fenêtre de contexte : Ollama appliquait sa valeur par défaut, 4 096 tokens dans le lab, alors que le contexte du RAG (15 chunks, prompt système et réponse) peut la dépasser. Ollama tronque alors sans erreur. main.py demande maintenant ses fenêtres explicitement : LLM_NUM_CTX (16 384, identique à la synthèse Teams, pour éviter un rechargement du modèle) et JUDGE_NUM_CTX (8 192, y compris pour l’appel qui maintient le juge en mémoire).
Ne pas utiliser
OLLAMA_CONTEXT_LENGTHsur le serveur Ollama. Ce réglage s’applique à tous les modèles, y compris le juge, dont il augmente la mémoire sans raison. Et une valeur écrite à la française, avec une espace fine insécable (« 16 384 »), est ignorée sans erreur : Ollama affiche un avertissement, visible seulement avecollama ps, et revient à sa valeur par défaut.
Sur la carte graphique de 16 Go prévue pour le §10, les fenêtres actuelles ne tiendront pas ensemble (16,3 Go mesurés sur CPU) : leur dimensionnement sera fait au §10, sur la base des tokens réellement consommés.
Le tenant et le serveur de fichiers du lab ont servi à de nombreux essais avant ce guide, comme ceux de beaucoup de PME qui ont testé des outils d’IA. Construire l’indexation y a mis à jour des situations typiques d’un tel environnement. La colonne « Origine » dit honnêtement d’où elles viennent : plusieurs ont été créées par nos propres essais, ce qui ne les rend pas moins représentatives. Elles sont reprises sous forme de recommandations dans la section gouvernance.
| Situation | Origine dans le lab | Risque | Traitement |
|---|---|---|---|
Ancienne application d’indexation avec Sites.Read.All |
Pilote abandonné (une plateforme RAG testée puis remplacée) | Lecture de tout le tenant, certificat valide deux ans | Supprimée |
| Compte d’indexation membre des groupes de droits Purview | Configuration initiale du lab | Droit de déchiffrer les documents financiers et RH | Retiré (et §5.2.4 réécrite) |
| Groupes Purview servant aussi d’ACL NTFS | Configuration antérieure du serveur de fichiers | Le retrait a coupé la lecture de six fichiers : invisibles pour tous dans le RAG, sans fuite | Groupe dédié à l’indexation, script Set-AccesIndexationRAG.ps1 |
| Groupe supprimé encore membre de deux sites | Inconnue | Accès retiré sans que personne ne le sache | Signalé par l’indexeur à chaque passage |
| Copies en clair de documents chiffrés | Documents de test et de démonstration | Contenu sensible trouvable par le RAG | À supprimer ou déplacer |
| Fichier nommé comme une clé TLS dans le corpus | Inconnue | Secret possible, trouvable par le RAG | À sortir du partage |
Le point commun : un outil d’IA rend trouvable ce qui n’était que caché dans une arborescence. L’inventaire des permissions et le rapport de synchronisation sont les deux outils qui permettent de le voir avant les utilisateurs.
| Point | Vérification | Résultat attendu |
|---|---|---|
| Inventaire | Scripts de §13.2 | Bibliothèques de conservation et membres atypiques identifiés |
| Application | Sonde avant l’accord | 403 sur chaque site |
| Accès | Accorder-SitesSelected.ps1 -Lister |
L’application en read sur les seuls sites prévus |
| Sonde après accord | sonde_sharepoint.py |
Fichiers, permissions, groupes du site lisibles |
| Simulation | sp_indexer.py --dry-run |
autorises[] conformes à la matrice, chiffrés détectés |
| Incrémental | Second passage | Tous les fichiers « inchangés » |
| Synchronisation | /admin/sync |
Étape sharepoint en succès, BM25 reconstruit |
| Cloisonnement | Matrice avec trois comptes | Toutes les réponses conformes |
| Citations | Question sur un document SharePoint | Lien cliquable vers le document |
| Fenêtres de contexte | ollama ps |
Modèle principal et juge aux valeurs demandées |
| Retour au sommaire | Section précédente : §12 Synthèse des réunions Teams | Section suivante : §14 Documents protégés par Purview |
Validé en lab sur VM-RAG-LAB, septembre 2026, sur un tenant Microsoft 365 synchronisé par Entra Connect, avec qwen2.5:14b sur CPU.