# 07 — RBAC sur l'intranet (Approche A) > Comment l'intranet filtre les cartes affichées selon le rôle du user. --- ## 🎯 Principe **Approche A** : le filtrage RBAC se fait **côté intranet uniquement**, pas sur chaque outil. ### Comment ça marche ``` 1. User se logge sur intranet.seizeconsulting.com │ │ OAuth2-Proxy fait l'auth Zitadel ▼ 2. nginx reçoit la requête avec header X-Forwarded-Email │ │ sub_filter remplace __USER_EMAIL__ par hedi.kalboussi@... ▼ 3. Browser reçoit l'HTML avec const USER_EMAIL = "hedi.kalboussi@..." │ │ JavaScript fetch /roles-mapping.json ▼ 4. JS détermine le rôle : "consultant_junior" │ │ JS itère sur les cartes ▼ 5. JS cache les cartes que ce rôle n'a pas le droit de voir │ ▼ 6. User voit uniquement Gitea + Code Server ``` ### Limite acceptée ⚠️ **Un user qui connaît `n8n.seizeconsulting.com` peut bypass le filtre** (l'outil reste accessible via URL directe). C'est acceptable pour Seize (88 employés non-malveillants). Pour un vrai RBAC, voir [DECISIONS.md D7](../DECISIONS.md). --- ## 📁 Structure des fichiers ``` ~/docker/intranet/ ├── docker-compose.yml ├── nginx-rbac.conf # ⭐ Config nginx avec sub_filter ├── index.html # ⭐ HTML avec data-app + JS RBAC ├── roles-mapping.json # ⭐ Mapping email → rôle (généré) └── logo.png ``` --- ## 📄 Config nginx (nginx-rbac.conf) ```nginx events {} http { server { listen 80; server_name _; root /usr/share/nginx/html; index index.html; # Désactiver le cache pour l'HTML location = /index.html { add_header Cache-Control "no-store, no-cache, must-revalidate"; # ⭐ Substituer __USER_EMAIL__ par la vraie valeur du header HTTP sub_filter '__USER_EMAIL__' '$http_x_forwarded_email'; sub_filter_once off; sub_filter_types text/html; } # Pareil pour la racine / location = / { add_header Cache-Control "no-store, no-cache, must-revalidate"; sub_filter '__USER_EMAIL__' '$http_x_forwarded_email'; sub_filter_once off; sub_filter_types text/html; try_files /index.html =404; } # Le mapping JSON doit être servi normalement location = /roles-mapping.json { add_header Cache-Control "no-store"; } # Tout le reste (logo, favicon, etc.) location / { try_files $uri $uri/ =404; } } } ``` ### Pourquoi sub_filter ? - `$http_x_forwarded_email` lit le header HTTP envoyé par OAuth2-Proxy - `sub_filter` substitue `__USER_EMAIL__` par cette valeur dans le HTML servi - `sub_filter_once off` : remplace **toutes les occurrences** (pas juste la 1ère) - `Cache-Control: no-store` : crucial pour que le HTML soit re-généré à chaque requête (sinon un user reçoit l'email d'un autre) --- ## 📄 HTML — Structure de chaque carte ```html
Portainer
Console de gestion des conteneurs Docker.
portainer.seizeconsulting.com Infra
``` → Chaque carte a un attribut **`data-app=""`** que le JavaScript utilise pour la matcher avec la matrice RBAC. --- ## 📄 JavaScript RBAC (à la fin de index.html) ```html ``` --- ## 🎭 Création des 8 rôles dans Zitadel ### Étape 1 — Aller dans le project Console Zitadel → Projects → **Infra Seize** → Roles → New ### Étape 2 — Créer un par un | Key | Display Name | Group | |---|---|---| | `direction` | Direction | hierarchie | | `manager` | Manager | hierarchie | | `expert` | Expert | hierarchie | | `consultant_senior` | Consultant Senior | hierarchie | | `consultant_junior` | Consultant Junior | hierarchie | | `alternant` | Alternant | hierarchie | | `stagiaire` | Stagiaire | hierarchie | | `support` | Support | hierarchie | ### Étape 3 — Activer "Return user roles during authentication" Console Zitadel → Projects → **Infra Seize** → General → Settings : - ☑️ **Return user roles during authentication** - ❌ **Only authorized users can authenticate** (à laisser DÉCOCHÉ — cf. [D9](../DECISIONS.md)) - ❌ **Authentication restricted to authorized orgs** ### Étape 4 — Configurer chaque App pour transmettre les rôles Pour chaque application (Intranet, n8n, Portainer, etc.) : Console Zitadel → Projects → Infra Seize → Applications → `` → Token Settings : - Auth Token Type : **JWT** (pas Bearer Token) - ☑️ User roles inside ID Token - ☑️ Include user's profile info in the ID Token - ☑️ Add user roles to the access token **Save**. --- ## 📊 Matrice RBAC effective | Outil | direction | manager | expert | cons_sr | cons_jr | alternant | stagiaire | support | |---|---|---|---|---|---|---|---|---| | **Intranet** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | **Gitea** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | **Code Server** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | **n8n** | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | | **Portainer** | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | | **Uptime Kuma (Status)** | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | | **Traefik Dashboard** | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | Voir aussi `matrices/rbac-matrix.md` pour la vue détaillée. --- ## 🔄 OAuth2-Proxy — Activer le scope des rôles Modifier le `.env` de **chaque** OAuth2-Proxy pour demander le scope des rôles : ```env OAUTH2_PROXY_SCOPE=openid email profile urn:zitadel:iam:org:project:roles ``` Puis restart : ```bash cd ~/docker/oauth2-proxy- docker compose down docker compose up -d ``` --- ## 🎬 Workflow complet : du recrutement au compte fonctionnel ``` 1. Nouveau collaborateur arrive │ ▼ 2. RH crée la fiche dans Furious Squad (job_title, manager, BU, etc.) │ │ Le jour J ou J+1 ▼ 3. Sync Furious → Zitadel - Soit manuel : python3 import-furious-to-zitadel.py - Soit auto : workflow n8n (à mettre en place) │ ▼ 4. Génération du mapping : python3 generate-roles-mapping.py → roles-mapping.json mis à jour │ ▼ 5. Copie vers l'intranet : cp roles-mapping.json ~/docker/intranet/ → nginx ne nécessite pas de restart (volume monté) │ ▼ 6. Attribution du rôle : python3 assign-roles.py → Le user reçoit son rôle dans Zitadel │ ▼ 7. Distribution du mdp temporaire via Teams │ ▼ 8. User se logge → voit les bonnes cartes selon son rôle ``` --- ## 🐛 Troubleshooting RBAC ### "Toutes les cartes sont visibles, le filtrage ne marche pas" #### Vérification 1 : le sub_filter nginx fonctionne ? ```bash # Depuis le serveur, vérifier que le HTML rendu contient le bon email docker exec intranet sh -c "curl -s -H 'X-Forwarded-Email: test@seizeconsulting.com' http://localhost/ | grep 'USER_EMAIL'" # Doit afficher : const USER_EMAIL = "test@seizeconsulting.com"; ``` Si le HTML contient encore `__USER_EMAIL__` : - Vérifier que `nginx-rbac.conf` est bien monté dans le conteneur - Vérifier les logs nginx : `docker logs intranet` #### Vérification 2 : roles-mapping.json est chargé ? Ouvrir la console JS du browser (F12) : ``` [RBAC] User: xxx | Rôle: xxx ``` Si Rôle = `null` : - Vérifier que l'email est bien dans roles-mapping.json - Attention à la casse (le script fait `.toLowerCase()`) #### Vérification 3 : la matrice JS est correcte ? Dans la console : ```js document.querySelectorAll('.service-card[data-app]').forEach(c => console.log(c.dataset.app, c.style.display)); ``` Doit afficher chaque carte avec son `display: none` ou `display: ""`. ### "Le user voit __USER_EMAIL__ littéralement" Cause : nginx ne fait pas le sub_filter. Solutions : 1. Vérifier que `nginx-rbac.conf` est bien monté en RO sur `/etc/nginx/nginx.conf` (pas un autre chemin) 2. Vérifier que le header `X-Forwarded-Email` arrive bien (debug : tail logs nginx) 3. Restart : `docker compose down && docker compose up -d` ### "Le user voit ses cartes mais quand il clique sur une carte non-autorisée, il y accède quand même" C'est **normal** et c'est la limite acceptée de l'Approche A. Pour vrai RBAC, voir [DECISIONS.md D7](../DECISIONS.md). ### "Un user vient d'être ajouté dans Furious mais ne voit aucune carte" Causes : 1. roles-mapping.json pas à jour → relancer `generate-roles-mapping.py` + copier 2. User pas encore dans Zitadel → relancer l'import 3. job_title pas dans le mapping → ajouter dans `JOB_TITLE_TO_ROLE` du script --- ## 🔄 Mise à jour de la matrice RBAC ### Cas 1 : Ajouter un nouvel outil 1. Ajouter la carte HTML dans `index.html` avec `data-app="newapp"` 2. Ajouter la ligne dans `ACCESS_MATRIX` : ```javascript "newapp": ["direction", "manager"], ``` 3. Restart : `cd ~/docker/intranet && docker compose restart` ### Cas 2 : Changer les autorisations d'un outil 1. Modifier la liste de rôles dans `ACCESS_MATRIX` du JS 2. Restart : `docker compose restart` 3. Tester avec différents profils ### Cas 3 : Ajouter un nouveau rôle 1. Créer le rôle dans Zitadel (Projects → Roles → New) 2. Mettre à jour le mapping dans `assign-roles.py` (variable `JOB_TITLE_TO_ROLE`) 3. Mettre à jour `ACCESS_MATRIX` dans le JS pour préciser ce que ce rôle voit 4. Lancer `generate-roles-mapping.py` + `assign-roles.py` 5. Copier `roles-mapping.json` vers l'intranet 6. Restart intranet --- ## 🎨 Personnalisation visuelle Le CSS de l'intranet est dans `