--- # Devis d'identite — RELEVE seul. Ne modifie rien (D-23 : un devis se lit d'abord). # # Le playbook collecte deux dictionnaires — ce que le depot DECLARE, ce que la machine # PORTE — et les depose en JSON sur le controleur. La comparaison est faite par # `scripts/devis_identite.py` : elle n'a rien a faire en Jinja, et le depot a deja # cette forme (devis_opnsense, devis_sdn : Python raisonne, Ansible releve). # # Pourquoi ce devis existe. Les 30 preuves de `prouver.py` sont STATIQUES : elles # montrent que le depot est coherent avec lui-meme. Aucune ne demande au systeme # deploye s'il ressemble a ce que le depot annonce. Les quatre defauts du 2026-08-08 # etaient tous de ce second type — chacun trouve en relisant apres avoir ecrit, # aucun signale par un test. # # make identite-plan - name: Devis d'identité — relever le déclaré et le réel hosts: serveur_keycloak become: true gather_facts: false vars: kc: "http://localhost:8080" devis_identite_sortie: "{{ playbook_dir }}/../../instance/devis-identite.json" tasks: # PRECEDENCE — `include_vars` au niveau du play PRIME sur les `group_vars`. # Charger les defauts d'un role tels quels ferait donc mentir le devis : il # comparerait au defaut du depot au lieu de la valeur reellement declaree par # l'inventaire. Mesure du 2026-08-08 : le premier jet rapportait # `client_pki_reload_services: []` sur les 14 hotes alors que quatre groupes le # declarent. Le devis portait exactement le defaut qu'il est cense traquer. # # On retient donc ce que l'inventaire declare AVANT de charger les defauts, puis # on le reimpose. (Charger sous un `name:` ne marche pas : les defauts se citent # entre eux — `serveur_keycloak_cert` reference `serveur_keycloak_steppath` — et ces # references ne se resolvent plus une fois enfermees dans un dictionnaire.) - name: Retenir ce que l'inventaire déclare pour serveur_keycloak ansible.builtin.set_fact: devis_inventaire: >- {{ hostvars[inventory_hostname] | dict2items | selectattr('key', 'match', '^serveur_keycloak_') | items2dict }} - name: Charger les défauts du rôle serveur_keycloak ansible.builtin.include_vars: dir: "{{ playbook_dir }}/../../roles/serveur_keycloak/defaults" # Cle par cle : `set_fact` ne prend pas un dictionnaire entier en argument libre — # il l'accepte sans erreur et n'en fait rien. C'est ainsi que le premier correctif # a paru fonctionner tout en laissant le devis mentir. - name: Rendre le dernier mot à l'inventaire ansible.builtin.set_fact: "{{ item.key }}": "{{ item.value }}" loop: "{{ devis_inventaire | dict2items }}" loop_control: label: "{{ item.key }}" - name: Résoudre la politique de mot de passe (déclaration unique) ansible.builtin.include_role: name: resoudre_politique_mdp # Meme raison : les DN de l'annuaire ne se recopient pas, ils se resolvent. - name: Résoudre la connexion à l'annuaire ansible.builtin.include_role: name: resoudre_annuaire - name: Charger le registre des applications ansible.builtin.include_vars: file: "{{ setops_plan_dir }}/applications.yml" - name: Obtenir un jeton d'administration ansible.builtin.uri: url: "{{ kc }}/realms/master/protocol/openid-connect/token" method: POST body_format: form-urlencoded body: grant_type: password client_id: admin-cli username: "{{ serveur_keycloak_admin_user }}" password: "{{ serveur_keycloak_admin_password }}" register: devis_jeton no_log: true - name: Lire le realm ansible.builtin.uri: url: "{{ kc }}/admin/realms/{{ serveur_keycloak_realm }}" headers: Authorization: "Bearer {{ devis_jeton.json.access_token }}" register: devis_realm no_log: true - name: Lire la fédération LDAP ansible.builtin.uri: url: "{{ kc }}/admin/realms/{{ serveur_keycloak_realm }}/components?type=org.keycloak.storage.UserStorageProvider" headers: Authorization: "Bearer {{ devis_jeton.json.access_token }}" register: devis_fed no_log: true - name: Lire les mappeurs de la fédération ansible.builtin.uri: url: >- {{ kc }}/admin/realms/{{ serveur_keycloak_realm }}/components?parent={{ devis_fed.json[0].id }}&type=org.keycloak.storage.ldap.mappers.LDAPStorageMapper headers: Authorization: "Bearer {{ devis_jeton.json.access_token }}" register: devis_mappeurs no_log: true - name: Lire la politique de mot de passe de l'annuaire community.general.ldap_search: dn: "ou=policies,{{ resoudre_annuaire_base_dn }}" scope: onelevel filter: "(objectClass=pwdPolicy)" server_uri: "ldapi:///" register: devis_ppolicy changed_when: false # Un compte sans adresse ne peut pas recevoir de lien de reinitialisation : pour # lui, « mot de passe oublie » est un ecran qui ne mene nulle part, et le seul # recours redevient l'exploitant manipulant le mot de passe d'autrui. - name: Lire les comptes de l'annuaire community.general.ldap_search: dn: "{{ resoudre_annuaire_users_dn }}" scope: onelevel filter: "(objectClass=inetOrgPerson)" attrs: ["uid", "mail", "givenName"] server_uri: "ldapi:///" register: devis_comptes changed_when: false - name: Déposer le relevé sur le contrôleur ansible.builtin.copy: dest: "{{ devis_identite_sortie }}" mode: "0600" content: "{{ { 'hote': inventory_hostname, 'realm': serveur_keycloak_realm, 'declare': { 'passwordPolicy': resoudre_politique_mdp_keycloak, 'brute': resoudre_politique_mdp_brute, 'ppolicy': resoudre_politique_mdp_ldap, 'durcissement': serveur_keycloak_ldap_durcissement, 'mappeurs': serveur_keycloak_ldap_attributs, 'editMode': serveur_keycloak_ldap_edit_mode, 'resetPasswordAllowed': serveur_keycloak_reset_mot_de_passe | bool, 'smtpHost': applications[serveur_keycloak_smtp_app].hote ~ '.' ~ domaine_interne, }, 'reel': { 'realm': devis_realm.json, 'federation': devis_fed.json[0].config, 'mappeurs': dict(devis_mappeurs.json | map(attribute='name') | list | zip(devis_mappeurs.json | map(attribute='config') | list)), 'ppolicy': (devis_ppolicy.results | first) | default({}), 'comptes': devis_comptes.results, }, } | to_nice_json }}" delegate_to: localhost become: false - name: Indiquer le relevé ansible.builtin.debug: msg: "Relevé déposé : {{ devis_identite_sortie }} — comparaison par scripts/devis_identite.py"