RBAC via SSO : rôle de realm Keycloak → niveau Grafana (authZ)

Machinerie additive/idempotente dans serveur_keycloak (rbac-oidc.yml) :
rôles de realm + mapper 'roles' sur les clients choisis + assignations
rôle→utilisateur. kcadm à chaud (zéro coupure SSO). Grafana :
role_attribute_path (grafana-admin→Admin, grafana-editor→Editor, sinon
Viewer). Prouvé sur id-sso-01 (idempotence) : testmail = grafana-editor.

Complète le dashboard logs (Viewer-friendly) : les deux volets de la
question « testmail peut-il voir les logs ? ».

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Daniel Allaire 2026-07-04 16:58:15 -04:00
parent 4887d2aee8
commit c5df8fac4a
6 changed files with 130 additions and 0 deletions

View file

@ -1,5 +1,22 @@
# CHANGELOG — Set-OPS
## 2026-07-04
### Ajouté
- **UI des logs Loki : dashboard Grafana provisionné.** Loki n'a pas d'UI ; son UI est Grafana.
Ajout d'un **dashboard « Journaux de la flotte »** (dossier Set-OPS) : sélecteur d'hôte multi +
filtre regex insensible à la casse + panneau logs + débit par hôte. Référence Loki par une
**variable de datasource** (`ds_loki`), pas un UID codé en dur (leçon : ajouter un uid explicite à
une datasource déjà provisionnée casse le démarrage de Grafana). **Visible par les Viewers** (dont
testmail) sans accès Explore. Prouvé sur obs-01 (dashboard chargé, Grafana actif).
- **RBAC via SSO : rôle de realm → niveau Grafana.** Machinerie *additive et idempotente* dans
`serveur_keycloak` (`rbac-oidc.yml`) : rôles de realm (`serveur_keycloak_realm_roles`), **mapper
`roles`** sur les clients choisis (`_role_mapper_clients`, rôles de realm → claim `roles` dans
ID token + userinfo), **assignations** rôle→utilisateur (`_role_assignments`). Côté Grafana,
`role_attribute_path` (`grafana-admin→Admin`, `grafana-editor→Editor`, sinon Viewer). kcadm **à
chaud, zéro coupure SSO**. Prouvé (idempotence `changed=0`) sur id-sso-01 : rôles créés, mapper
présent, `testmail` = `grafana-editor` (→ Explore). Illustre l'**authZ** (vs authN du SSO).
## 2026-07-03
### Ajouté

View file

@ -29,6 +29,9 @@ serveur_grafana_oidc_client_secret: "{{ vault_grafana_oidc | default('') }}" #
serveur_grafana_oidc_realm: "chezlepro"
serveur_grafana_oidc_base: "https://keycloak.{{ domaine_interne }}/realms/{{ serveur_grafana_oidc_realm }}/protocol/openid-connect"
serveur_grafana_oidc_scopes: "openid email profile"
# RBAC via SSO : mappe le claim 'roles' (rôles de realm Keycloak) vers le niveau Grafana.
# Défaut : grafana-admin→Admin, grafana-editor→Editor, sinon Viewer. Vider pour désactiver.
serveur_grafana_oidc_role_path: "contains(roles[*], 'grafana-admin') && 'Admin' || contains(roles[*], 'grafana-editor') && 'Editor' || 'Viewer'"
# Depot apt officiel Grafana (partage avec serveur_loki).
serveur_grafana_depot_cle_url: "https://apt.grafana.com/gpg-full.key"

View file

@ -13,4 +13,8 @@ Environment=GF_AUTH_GENERIC_OAUTH_AUTH_URL={{ serveur_grafana_oidc_base }}/auth
Environment=GF_AUTH_GENERIC_OAUTH_TOKEN_URL={{ serveur_grafana_oidc_base }}/token
Environment=GF_AUTH_GENERIC_OAUTH_API_URL={{ serveur_grafana_oidc_base }}/userinfo
Environment=GF_AUTH_GENERIC_OAUTH_LOGIN_ATTRIBUTE_PATH=preferred_username
{% if serveur_grafana_oidc_role_path %}
Environment="GF_AUTH_GENERIC_OAUTH_ROLE_ATTRIBUTE_PATH={{ serveur_grafana_oidc_role_path }}"
Environment=GF_AUTH_GENERIC_OAUTH_ROLE_ATTRIBUTE_STRICT=false
{% endif %}
{% endif %}

View file

@ -49,3 +49,12 @@ serveur_keycloak_truststore_path: "/etc/ssl/certs/ca-certificates.crt"
# web_origins: ["https://grafana.{{ domaine_interne }}"]
# secret: "{{ vault_grafana_oidc }}"
serveur_keycloak_clients: []
# --- RBAC OIDC (additif, idempotent) ---
# Rôles de realm à garantir (ex. ['grafana-admin', 'grafana-editor']).
serveur_keycloak_realm_roles: []
# Clients recevant le mapper « roles » (rôles de realm → claim 'roles' dans le jeton).
serveur_keycloak_role_mapper_clients: []
# Assignations rôle→utilisateur (ex. [{user: testmail, role: grafana-editor}]).
# En prod, préférer l'assignation via groupe d'annuaire ; ici, explicite pour la preuve.
serveur_keycloak_role_assignments: []

View file

@ -146,6 +146,12 @@
ansible.builtin.include_tasks: clients-oidc.yml
when: serveur_keycloak_clients | length > 0
- name: Configurer le RBAC OIDC (roles de realm + mapper + assignations)
ansible.builtin.include_tasks: rbac-oidc.yml
when: >
serveur_keycloak_realm_roles | length > 0
or serveur_keycloak_role_assignments | length > 0
- name: Definir le theme de login du realm (identite Alliance Boreale)
ansible.builtin.shell:
executable: /bin/bash

View file

@ -0,0 +1,91 @@
---
# RBAC OIDC : rôles de realm + mapper de rôles sur les clients concernés + assignations.
# Additif et idempotent (create-si-absent, kcadm à chaud — pas de restart Keycloak).
# Permet à une app (ex. Grafana) de mapper un rôle du realm vers son niveau de droit.
- name: Assurer les roles de realm (kcadm, idempotent)
ansible.builtin.shell:
executable: /bin/bash
cmd: |
set -euo pipefail
KC={{ serveur_keycloak_home }}/bin/kcadm.sh
"$KC" config credentials --server http://localhost:8080 --realm master \
--user {{ serveur_keycloak_admin_user }} --password "$KC_ADMIN_PW" >/dev/null
if "$KC" get roles/{{ item }} -r {{ serveur_keycloak_realm }} >/dev/null 2>&1; then
echo SETOPS_OK
else
"$KC" create roles -r {{ serveur_keycloak_realm }} -s name={{ item }} >/dev/null
echo SETOPS_CHANGED
fi
environment:
KC_ADMIN_PW: "{{ serveur_keycloak_admin_password }}"
loop: "{{ serveur_keycloak_realm_roles }}"
register: serveur_keycloak_role_reg
changed_when: "'SETOPS_CHANGED' in serveur_keycloak_role_reg.stdout"
no_log: true
when:
- not ansible_check_mode
- serveur_keycloak_realm_roles | length > 0
- name: Assurer le mapper de roles de realm sur les clients (claim 'roles')
ansible.builtin.shell:
executable: /bin/bash
cmd: |
set -euo pipefail
KC={{ serveur_keycloak_home }}/bin/kcadm.sh
"$KC" config credentials --server http://localhost:8080 --realm master \
--user {{ serveur_keycloak_admin_user }} --password "$KC_ADMIN_PW" >/dev/null
CID=$("$KC" get clients -r {{ serveur_keycloak_realm }} -q clientId={{ item }} 2>/dev/null \
| grep -oP '"id"\s*:\s*"\K[^"]+' | head -1)
if [ -z "$CID" ]; then echo "SETOPS_OK client absent"; exit 0; fi
if "$KC" get clients/$CID/protocol-mappers/models -r {{ serveur_keycloak_realm }} 2>/dev/null \
| grep -q '"realm-roles"'; then
echo SETOPS_OK
else
"$KC" create clients/$CID/protocol-mappers/models -r {{ serveur_keycloak_realm }} \
-s name=realm-roles -s protocol=openid-connect \
-s protocolMapper=oidc-usermodel-realm-role-mapper \
-s 'config."claim.name"=roles' \
-s 'config."jsonType.label"=String' \
-s 'config."multivalued"=true' \
-s 'config."id.token.claim"=true' \
-s 'config."access.token.claim"=true' \
-s 'config."userinfo.token.claim"=true' >/dev/null
echo SETOPS_CHANGED
fi
environment:
KC_ADMIN_PW: "{{ serveur_keycloak_admin_password }}"
loop: "{{ serveur_keycloak_role_mapper_clients }}"
register: serveur_keycloak_mapper_reg
changed_when: "'SETOPS_CHANGED' in serveur_keycloak_mapper_reg.stdout"
no_log: true
when:
- not ansible_check_mode
- serveur_keycloak_role_mapper_clients | length > 0
- name: Assigner les roles de realm aux utilisateurs (add-roles idempotent)
ansible.builtin.shell:
executable: /bin/bash
cmd: |
set -euo pipefail
KC={{ serveur_keycloak_home }}/bin/kcadm.sh
"$KC" config credentials --server http://localhost:8080 --realm master \
--user {{ serveur_keycloak_admin_user }} --password "$KC_ADMIN_PW" >/dev/null
if "$KC" get-roles -r {{ serveur_keycloak_realm }} --uusername {{ item.user }} 2>/dev/null \
| grep -q '"{{ item.role }}"'; then
echo SETOPS_OK
else
"$KC" add-roles -r {{ serveur_keycloak_realm }} --uusername {{ item.user }} --rolename {{ item.role }} >/dev/null
echo SETOPS_CHANGED
fi
environment:
KC_ADMIN_PW: "{{ serveur_keycloak_admin_password }}"
loop: "{{ serveur_keycloak_role_assignments }}"
loop_control:
label: "{{ item.user }} -> {{ item.role }}"
register: serveur_keycloak_assign_reg
changed_when: "'SETOPS_CHANGED' in serveur_keycloak_assign_reg.stdout"
no_log: true
when:
- not ansible_check_mode
- serveur_keycloak_role_assignments | length > 0