- 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
404 lines
12 KiB
Markdown
404 lines
12 KiB
Markdown
# 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](../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
|
|
<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)
|
|
|
|
```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](../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 → `<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 :
|
|
|
|
```env
|
|
OAUTH2_PROXY_SCOPE=openid email profile urn:zitadel:iam:org:project:roles
|
|
```
|
|
|
|
Puis restart :
|
|
|
|
```bash
|
|
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 ?
|
|
|
|
```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 `<style>` au début d'`index.html`. Couleurs principales :
|
|
|
|
```css
|
|
: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](./06-import-furious.md) — Import des users (étape préalable)
|
|
- [04-oauth2-proxy-pattern.md](./04-oauth2-proxy-pattern.md) — Comment OAuth2-Proxy transmet les headers
|
|
- [DECISIONS.md D7](../DECISIONS.md) — Pourquoi Approche A
|
|
- [matrices/rbac-matrix.md](../matrices/rbac-matrix.md) — Matrice complète
|
|
- [matrices/job-title-to-role.csv](../matrices/job-title-to-role.csv) — Mapping Furious → rôle
|