acces : Forgejo et Nextcloud cables, et le claim groups emis

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 <noreply@anthropic.com>
This commit is contained in:
Daniel Allaire 2026-08-07 14:49:28 -04:00
parent 74a2456f09
commit a08c61bb4e
9 changed files with 154 additions and 17 deletions

View file

@ -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.

View file

@ -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"

View file

@ -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

View file

@ -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 }}"

View file

@ -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: []

View file

@ -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

View file

@ -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"

View file

@ -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

View file

@ -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