- 6 docs racine (README, HANDOVER, ARCHITECTURE, DECISIONS, INSTALL, OPERATIONS) - 9 docs thématiques dans docs/ - 10 templates docker-compose dans configs/ - 7 scripts Python/Shell d'import Furious vers Zitadel - 2 matrices RBAC Etat infra au 5 juin 2026 : - 88 users Furious importes dans Zitadel - 8 roles RBAC crees et attribues - Intranet filtre les cartes selon role (Approche A) - 7 outils SSO operationnels
12 KiB
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 <a class="service-card" data-app="...">
▼
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.
📁 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)
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_emaillit le header HTTP envoyé par OAuth2-Proxysub_filtersubstitue__USER_EMAIL__par cette valeur dans le HTML servisub_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
<a class="service-card"
data-app="portainer" ← ⭐ ATTRIBUT RBAC
data-color="teal"
href="https://portainer.seizeconsulting.com"
target="_blank" rel="noopener">
<div class="card-header">
<div class="card-icon"><!-- SVG --></div>
<div class="card-arrow"><!-- SVG --></div>
</div>
<div class="card-body">
<div class="card-title">Portainer</div>
<div class="card-desc">Console de gestion des conteneurs Docker.</div>
<div class="card-meta">
<span class="card-url">portainer.seizeconsulting.com</span>
<span class="card-tag">Infra</span>
</div>
</div>
</a>
→ Chaque carte a un attribut data-app="<nom>" que le JavaScript utilise pour la matcher avec la matrice RBAC.
📄 JavaScript RBAC (à la fin de index.html)
<script>
/* ===== RBAC INTRANET — Filtrage cartes selon rôle Zitadel ===== */
(async function() {
const USER_EMAIL = "__USER_EMAIL__"; // ← Remplacé par nginx sub_filter
/* Matrice RBAC : qui voit quelle carte */
const ACCESS_MATRIX = {
"portainer": ["direction", "manager"],
"status": ["direction"],
"n8n": ["direction", "manager", "expert"],
"gitea": ["direction", "manager", "expert", "consultant_senior", "consultant_junior", "alternant", "stagiaire", "support"],
"code": ["direction", "manager", "expert", "consultant_senior", "consultant_junior", "alternant", "stagiaire", "support"],
"traefik": ["direction"]
};
let userRole = null;
try {
const response = await fetch("/roles-mapping.json", { cache: "no-store" });
if (response.ok) {
const mapping = await response.json();
userRole = mapping[USER_EMAIL.toLowerCase()] || null;
}
} catch (e) {
console.error("[RBAC] Erreur chargement mapping:", e);
}
console.log("[RBAC] User:", USER_EMAIL, "| Rôle:", userRole);
/* Filtrer les cartes selon le rôle */
document.querySelectorAll(".service-card[data-app]").forEach(card => {
const app = card.dataset.app;
const allowedRoles = ACCESS_MATRIX[app] || [];
if (!userRole || !allowedRoles.includes(userRole)) {
card.style.display = "none";
}
});
})();
</script>
🎭 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)
- ❌ 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 → <App> → 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 :
OAUTH2_PROXY_SCOPE=openid email profile urn:zitadel:iam:org:project:roles
Puis restart :
cd ~/docker/oauth2-proxy-<outil>
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 ?
# 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.confest 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 :
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 :
- Vérifier que
nginx-rbac.confest bien monté en RO sur/etc/nginx/nginx.conf(pas un autre chemin) - Vérifier que le header
X-Forwarded-Emailarrive bien (debug : tail logs nginx) - 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.
"Un user vient d'être ajouté dans Furious mais ne voit aucune carte"
Causes :
- roles-mapping.json pas à jour → relancer
generate-roles-mapping.py+ copier - User pas encore dans Zitadel → relancer l'import
- job_title pas dans le mapping → ajouter dans
JOB_TITLE_TO_ROLEdu script
🔄 Mise à jour de la matrice RBAC
Cas 1 : Ajouter un nouvel outil
- Ajouter la carte HTML dans
index.htmlavecdata-app="newapp" - Ajouter la ligne dans
ACCESS_MATRIX:"newapp": ["direction", "manager"], - Restart :
cd ~/docker/intranet && docker compose restart
Cas 2 : Changer les autorisations d'un outil
- Modifier la liste de rôles dans
ACCESS_MATRIXdu JS - Restart :
docker compose restart - Tester avec différents profils
Cas 3 : Ajouter un nouveau rôle
- Créer le rôle dans Zitadel (Projects → Roles → New)
- Mettre à jour le mapping dans
assign-roles.py(variableJOB_TITLE_TO_ROLE) - Mettre à jour
ACCESS_MATRIXdans le JS pour préciser ce que ce rôle voit - Lancer
generate-roles-mapping.py+assign-roles.py - Copier
roles-mapping.jsonvers l'intranet - Restart intranet
🎨 Personnalisation visuelle
Le CSS de l'intranet est dans <style> au début d'index.html. Couleurs principales :
:root {
--c-bg-deep: #071C1C; /* Fond sombre */
--c-accent: #008A81; /* Vert Seize */
--c-accent-bright: #00C9B7;
--c-mint: #A9D4D3;
--c-lime: #F3FBBA;
--c-cream: #FBFBF4;
}
Pour changer le logo : remplacer ~/docker/intranet/logo.png.
📚 Voir aussi
- 06-import-furious.md — Import des users (étape préalable)
- 04-oauth2-proxy-pattern.md — Comment OAuth2-Proxy transmet les headers
- DECISIONS.md D7 — Pourquoi Approche A
- matrices/rbac-matrix.md — Matrice complète
- matrices/job-title-to-role.csv — Mapping Furious → rôle