From a08c61bb4e0d4f8fdc09b2cd0c1b5e38bcdd9310 Mon Sep 17 00:00:00 2001 From: Daniel Allaire Date: Fri, 7 Aug 2026 14:49:28 -0400 Subject: [PATCH] acces : Forgejo et Nextcloud cables, et le claim `groups` emis MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Les deux services n'etaient pas encore deployes : les cabler maintenant vaut mieux que les corriger apres. `porte_par` passe de `aucun` a `claim-groupe`. Forgejo recoit --group-claim-name + --admin-group, et sa tache passe de « creer si absent » a add-oauth OU update-oauth : le meme defaut que la federation Keycloak — cree une fois, jamais corrige — l'attendait sinon. Nextcloud recoit --mapping-groups + --group-provisioning, plus une tache qui verse les membres du groupe d'habilitation dans le groupe interne `admin` : etre dans un groupe projete ne donne aucun pouvoir en soi. Le maillon qui manquait aux deux : AUCUN mapper de protocole n'emettait le claim. Les groupes existaient dans le realm et n'apparaissaient dans aucun jeton — un cablage correct des deux cotes et rien au milieu. `oidc-group-membership-mapper` pose sur les trois clients, `full.path=false` pour que le claim porte `sysadmin` et non `/sysadmin`. Co-Authored-By: Claude Opus 5 --- CHANGELOG.md | 23 +++++++++++ roles/serveur_forgejo/defaults/main.yml | 11 +++++ roles/serveur_forgejo/meta/acces.yml | 11 ++--- roles/serveur_forgejo/tasks/main.yml | 25 ++++++++++-- roles/serveur_keycloak/defaults/main.yml | 6 +++ roles/serveur_keycloak/tasks/groupes-ldap.yml | 40 +++++++++++++++++++ roles/serveur_nextcloud/defaults/main.yml | 8 ++++ roles/serveur_nextcloud/meta/acces.yml | 10 ++--- roles/serveur_nextcloud/tasks/oidc.yml | 37 +++++++++++++++++ 9 files changed, 154 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a76612..98c190b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,29 @@ ## 2026-08-06 — le chemin nord-sud devient dérivable +### Forgejo et Nextcloud câblés — avant d'être déployés + +Les deux services n'existaient pas encore : les câbler maintenant vaut mieux que les corriger +après. `porte_par` passe de `aucun` à `claim-groupe` dans leurs `meta/acces.yml`. + +**Forgejo** reçoit `--group-claim-name` + `--admin-group` sur sa source OAuth2. Et sa tâche +est passée de « créer si absent » à **`add-oauth` ou `update-oauth`** : le même défaut que la +fédération Keycloak — créé une fois, jamais corrigé — l'attendait sinon. + +**Nextcloud** était déjà en upsert. Il reçoit `--mapping-groups` et `--group-provisioning`, +plus une tâche qui verse les membres du groupe d'habilitation dans le groupe interne `admin` : +être dans un groupe projeté ne donne aucun pouvoir en soi. L'absence du groupe au premier +déploiement n'est **pas** une erreur — il n'existe qu'à la première connexion d'un membre. + +**Le maillon qui manquait aux deux.** Les groupes existaient dans le realm mais +n'apparaissaient dans aucun jeton : pas de mapper de protocole. Forgejo et Nextcloud auraient +lu un claim vide et n'auraient rien accordé — un câblage correct des deux côtés, et rien au +milieu. `oidc-group-membership-mapper` est désormais posé sur les trois clients, avec +`full.path=false` pour que le claim porte `sysadmin` et non `/sysadmin`. + +Vérifié : `grafana`, `forgejo`, `nextcloud` portent chacun le mapper. + + ### La chaîne d'habilitation est complète `group-ldap-mapper` construit, et le rôle est **attaché au groupe** — pas à une personne. diff --git a/roles/serveur_forgejo/defaults/main.yml b/roles/serveur_forgejo/defaults/main.yml index 357dbe1..fa59a66 100644 --- a/roles/serveur_forgejo/defaults/main.yml +++ b/roles/serveur_forgejo/defaults/main.yml @@ -66,3 +66,14 @@ serveur_forgejo_oidc_discovery: "https://keycloak.{{ domaine_interne }}/realms/{ # le cert serveur contre le root_ca step-ca (via PGSSLROOTCERT dans l'unite systemd). serveur_forgejo_db_sslmode: "disable" serveur_forgejo_db_sslrootcert: "/etc/step/certs/root_ca.crt" + +# --- Habilitation par groupe (D-66) ------------------------------------------- +# Forgejo lit un claim de groupes dans le jeton OIDC et accorde l'administration +# aux membres d'un groupe nomme. Sans ces reglages, TOUT utilisateur authentifie +# obtient le niveau par defaut et l'administration reste au compte local de +# secours — ce que `meta/acces.yml` signalait comme `porte_par: aucun`. +# +# Le claim `groups` est celui que Keycloak emet pour les groupes projetes depuis +# LDAP (group-ldap-mapper). Voir docs/autorisation.md. +serveur_forgejo_oidc_groupe_claim: "groups" +serveur_forgejo_oidc_groupe_admin: "sysadmin" diff --git a/roles/serveur_forgejo/meta/acces.yml b/roles/serveur_forgejo/meta/acces.yml index 8704f18..e9191a3 100644 --- a/roles/serveur_forgejo/meta/acces.yml +++ b/roles/serveur_forgejo/meta/acces.yml @@ -3,13 +3,10 @@ acces: - groupe: sysadmin accorde: admin - # /!\ AUCUN mecanisme ne porte cette habilitation aujourd'hui : le role ne - # configure ni claim de groupe, ni mapping d'equipe. Toute personne qui - # s'authentifie obtient le niveau par defaut, et le compte administrateur - # reste le compte local de secours (voute). - # Forgejo sait lire un claim OIDC de groupes (`GROUP_CLAIM_NAME` + - # `ADMIN_GROUP`) : c'est le cablage manquant. Constate le 2026-08-07. - porte_par: aucun + # `--group-claim-name` + `--admin-group` sur la source OAuth2 : Forgejo lit le + # claim `groups` du jeton et accorde l'administration aux membres du groupe. + # Les reglages sont RECONCILIES a chaque passage, pas seulement poses. + porte_par: claim-groupe raison: >- Administration de la forge : utilisateurs, organisations, reglages. - groupe: dev diff --git a/roles/serveur_forgejo/tasks/main.yml b/roles/serveur_forgejo/tasks/main.yml index 02ad693..5baff8d 100644 --- a/roles/serveur_forgejo/tasks/main.yml +++ b/roles/serveur_forgejo/tasks/main.yml @@ -149,16 +149,33 @@ cmd: | set -euo pipefail FJ="{{ serveur_forgejo_binaire }} --config {{ serveur_forgejo_config }}" - if $FJ admin auth list 2>/dev/null | grep -qw "{{ serveur_forgejo_oidc_nom }}"; then - echo SETOPS_OK - else + # Les reglages de GROUPE sont RECONCILIES, pas seulement poses a la creation : + # `add-oauth` une fois puis plus rien laissait un cablage perime vivre + # indefiniment — c'est ce qui est arrive a la federation LDAP de Keycloak, qui + # a pointe un hote inexistant pendant des semaines (2026-08-07). + OPTS_GROUPE="" + {% if serveur_forgejo_oidc_groupe_admin | length > 0 %} + OPTS_GROUPE="--group-claim-name {{ serveur_forgejo_oidc_groupe_claim }} --admin-group {{ serveur_forgejo_oidc_groupe_admin }}" + {% endif %} + ID=$($FJ admin auth list 2>/dev/null \ + | awk -v n="{{ serveur_forgejo_oidc_nom }}" '$2 == n {print $1}' | head -1) + if [ -z "$ID" ]; then $FJ admin auth add-oauth \ --name "{{ serveur_forgejo_oidc_nom }}" \ --provider openidConnect \ --key "{{ serveur_forgejo_oidc_client_id }}" \ --secret "$OIDC_SECRET" \ - --auto-discover-url "{{ serveur_forgejo_oidc_discovery }}" >/dev/null + --auto-discover-url "{{ serveur_forgejo_oidc_discovery }}" \ + $OPTS_GROUPE >/dev/null echo SETOPS_CHANGED + else + # `update-oauth` est idempotent cote Forgejo : il reecrit les memes valeurs + # sans effet de bord. On ne peut pas comparer avant/apres (la CLI n'expose + # pas le detail d'une source), donc on ne signale PAS `changed`. + $FJ admin auth update-oauth --id "$ID" \ + --auto-discover-url "{{ serveur_forgejo_oidc_discovery }}" \ + $OPTS_GROUPE >/dev/null + echo SETOPS_OK fi environment: OIDC_SECRET: "{{ serveur_forgejo_oidc_client_secret }}" diff --git a/roles/serveur_keycloak/defaults/main.yml b/roles/serveur_keycloak/defaults/main.yml index a6678d1..355a216 100644 --- a/roles/serveur_keycloak/defaults/main.yml +++ b/roles/serveur_keycloak/defaults/main.yml @@ -89,3 +89,9 @@ serveur_keycloak_groupes_membership_attr: "member" # nominative : ajouter quelqu'un au groupe suffit, aucun deploiement n'est requis. # Les roles doivent exister (`serveur_keycloak_realm_roles`). serveur_keycloak_groupes_roles: [] + +# Le claim qui porte les groupes dans le jeton, et les clients qui le recoivent. +# Sans ce mapper de protocole, les groupes existent dans le realm mais n'atteignent +# jamais les services : Forgejo et Nextcloud liraient un claim vide. +serveur_keycloak_groupes_claim: "groups" +serveur_keycloak_groupes_mapper_clients: [] diff --git a/roles/serveur_keycloak/tasks/groupes-ldap.yml b/roles/serveur_keycloak/tasks/groupes-ldap.yml index 85eec1b..1a4965c 100644 --- a/roles/serveur_keycloak/tasks/groupes-ldap.yml +++ b/roles/serveur_keycloak/tasks/groupes-ldap.yml @@ -117,3 +117,43 @@ when: - serveur_keycloak_groupes_roles | length > 0 - not ansible_check_mode + +# Le mapper de PROTOCOLE : sans lui, les groupes existent dans le realm mais +# n'apparaissent JAMAIS dans le jeton — Forgejo et Nextcloud lisent un claim vide +# et n'accordent rien. C'est le dernier maillon de la chaine. +- name: Émettre le claim de groupes dans le jeton des clients + 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 '"groupes-membres"'; then + echo SETOPS_OK + else + # `full.path=false` : le claim porte `sysadmin`, pas `/sysadmin`. Les + # services comparent un nom de groupe, pas un chemin. + "$KC" create clients/$CID/protocol-mappers/models -r {{ serveur_keycloak_realm }} \ + -s name=groupes-membres -s protocol=openid-connect \ + -s protocolMapper=oidc-group-membership-mapper \ + -s 'config."claim.name"={{ serveur_keycloak_groupes_claim }}' \ + -s 'config."full.path"=false' \ + -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_groupes_mapper_clients }}" + register: serveur_keycloak_grp_mapper + changed_when: "'SETOPS_CHANGED' in serveur_keycloak_grp_mapper.stdout" + no_log: true + when: + - serveur_keycloak_groupes_mapper_clients | length > 0 + - not ansible_check_mode diff --git a/roles/serveur_nextcloud/defaults/main.yml b/roles/serveur_nextcloud/defaults/main.yml index e5b43d8..14158db 100644 --- a/roles/serveur_nextcloud/defaults/main.yml +++ b/roles/serveur_nextcloud/defaults/main.yml @@ -100,3 +100,11 @@ serveur_nextcloud_theme_ciel: true # thème custom « ciel boréal » (log # --- Sécurité : whitelister le sous-réseau d'admin (PAS désactiver l'anti-force-brute) --- serveur_nextcloud_bruteforce_whitelist: "{{ (sous_reseau_admin | default('')) }}" + +# --- Habilitation par groupe (D-66) ------------------------------------------- +# `user_oidc` provisionne les groupes du claim `groups` — ceux que Keycloak projette +# depuis LDAP (group-ldap-mapper). Etre dans un groupe ne donne pourtant aucun +# pouvoir : l'administration de Nextcloud est le groupe interne `admin`, ou le role +# verse les membres du groupe d'habilitation. +serveur_nextcloud_oidc_groupe_claim: "groups" +serveur_nextcloud_oidc_groupe_admin: "sysadmin" diff --git a/roles/serveur_nextcloud/meta/acces.yml b/roles/serveur_nextcloud/meta/acces.yml index 70d3eda..695b113 100644 --- a/roles/serveur_nextcloud/meta/acces.yml +++ b/roles/serveur_nextcloud/meta/acces.yml @@ -3,12 +3,10 @@ acces: - groupe: sysadmin accorde: admin - # /!\ AUCUN mecanisme ne porte cette habilitation aujourd'hui : le role ne - # configure pas de mapping de groupes OIDC. L'administration passe donc par - # le compte local de secours (voute, `occ`), ce qui contredit « une identite, - # un mot de passe ». Constate le 2026-08-07. - # `user_oidc` sait mapper un claim de groupes : c'est le cablage manquant. - porte_par: aucun + # `user_oidc --mapping-groups --group-provisioning` importe les groupes du + # claim, puis le role verse leurs membres dans le groupe interne `admin` — + # etre dans un groupe projete ne donne aucun pouvoir en soi. + porte_par: claim-groupe raison: >- Administration : utilisateurs, quotas, applications, partages. - groupe: personnel diff --git a/roles/serveur_nextcloud/tasks/oidc.yml b/roles/serveur_nextcloud/tasks/oidc.yml index 6fe7557..c1288ba 100644 --- a/roles/serveur_nextcloud/tasks/oidc.yml +++ b/roles/serveur_nextcloud/tasks/oidc.yml @@ -44,6 +44,43 @@ - "--mapping-uid=preferred_username" - "--mapping-email=email" - "--mapping-display-name=name" + - "--mapping-groups={{ serveur_nextcloud_oidc_groupe_claim }}" + - "--group-provisioning=1" - "--unique-uid=0" changed_when: false no_log: true + + # Les groupes projetes depuis LDAP arrivent dans Nextcloud, mais y etre membre + # ne donne aucun pouvoir : l'administration est un GROUPE NEXTCLOUD nomme + # `admin`. On y verse le groupe d'habilitation, une fois qu'il existe. + # + # Sans cela, `meta/acces.yml` restait `porte_par: aucun` et l'administration + # passait par le compte local de secours — ce qui contredit « une identite, un + # mot de passe » (2026-08-07). + - name: Verser le groupe d'administration dans le groupe `admin` de Nextcloud + ansible.builtin.shell: + executable: /bin/bash + cmd: | + set -eo pipefail + OCC="php {{ serveur_nextcloud_racine }}/occ" + G="{{ serveur_nextcloud_oidc_groupe_admin }}" + # Le groupe n'existe qu'apres la premiere connexion d'un membre : son + # absence n'est donc PAS une erreur au premier deploiement. + if ! $OCC group:list --output=json | grep -q "\"$G\""; then + echo "SETOPS_OK (groupe '$G' pas encore provisionne — a la premiere connexion)" + exit 0 + fi + CHANGED=0 + for U in $($OCC group:list --output=json | python3 -c " + import json,sys + print(' '.join((json.load(sys.stdin).get('$G') or [])))"); do + if ! $OCC group:list --output=json | python3 -c " + import json,sys + sys.exit(0 if '$U' in (json.load(sys.stdin).get('admin') or []) else 1)"; then + $OCC group:adduser admin "$U" >/dev/null && CHANGED=1 + fi + done + [ "$CHANGED" = 1 ] && echo SETOPS_CHANGED || echo SETOPS_OK + register: serveur_nextcloud_grp_admin + changed_when: "'SETOPS_CHANGED' in serveur_nextcloud_grp_admin.stdout" + when: serveur_nextcloud_oidc_groupe_admin | length > 0