From 2f992983f4171700386f2c46e73efc8fffe74703 Mon Sep 17 00:00:00 2001 From: Kevin Veen-Birkenbach Date: Fri, 12 Sep 2025 17:01:37 +0200 Subject: [PATCH] xwiki: install/verify via REST Job API; add 'xwiki_job_id' filter; refactor extension probe; remove invalid /extensions/{id} verify; README wording Context: fixed 404 on 'Verify OIDC extension is installed' by polling jobstatus and parsing job id via filter plugin. Conversation: https://chatgpt.com/share/68c435b7-96c0-800f-b7d6-b3fe99b443e0 --- roles/web-app-xwiki/README.md | 2 +- .../filter_plugins/xwiki_filters.py | 82 +++++++++++++++++++ roles/web-app-xwiki/tasks/04_extensions.yml | 75 +++++++++-------- .../web-app-xwiki/tasks/_probe_extension.yml | 22 +++++ 4 files changed, 145 insertions(+), 36 deletions(-) create mode 100644 roles/web-app-xwiki/filter_plugins/xwiki_filters.py create mode 100644 roles/web-app-xwiki/tasks/_probe_extension.yml diff --git a/roles/web-app-xwiki/README.md b/roles/web-app-xwiki/README.md index 8abaac2b..b364bddc 100644 --- a/roles/web-app-xwiki/README.md +++ b/roles/web-app-xwiki/README.md @@ -6,7 +6,7 @@ Empower your organization with **XWiki**, an open-source enterprise wiki and kno ## Overview -This role deploys XWiki using Docker, automating the installation, configuration, and management of your XWiki server. It integrates with an MariaDB database and an Nginx reverse proxy. The role supports advanced features such as global CSS injection, Matomo analytics, OIDC authentication, and centralized logout, making it a powerful and customizable solution within the Infinito.Nexus ecosystem. +This role deploys XWiki using Docker, automating the installation, configuration, and management of your XWiki server. It integrates with an relational database and a reverse proxy. The role supports advanced features such as global CSS injection, Matomo analytics, OIDC authentication, and centralized logout, making it a powerful and customizable solution within the Infinito.Nexus ecosystem. ## Features diff --git a/roles/web-app-xwiki/filter_plugins/xwiki_filters.py b/roles/web-app-xwiki/filter_plugins/xwiki_filters.py new file mode 100644 index 00000000..e28afa34 --- /dev/null +++ b/roles/web-app-xwiki/filter_plugins/xwiki_filters.py @@ -0,0 +1,82 @@ +# filter_plugins/xwiki_filters.py +from __future__ import annotations + +import re +from typing import Any, Dict, Optional, Iterable +try: + # Ansible provides this; don't hard-depend at import time for unit tests + from ansible.errors import AnsibleFilterError +except Exception: # pragma: no cover + class AnsibleFilterError(Exception): + pass + + +_JOB_LOC_RE = re.compile(r"/rest/jobstatus/([^?\s#]+)") + + +def _join_elements(elems: Iterable[Any]) -> str: + return "/".join(str(x) for x in elems) + + +def xwiki_job_id(response: Any, default: Optional[str] = None, strict: bool = False) -> Optional[str]: + """ + Extract a XWiki job ID from a typical Ansible `uri` response. + + Supports: + - JSON mapping: {"id": {"elements": ["install", "extensions", "123"]}} + - JSON mapping: {"id": "install/extensions/123"} + - Fallback from Location header or URL containing "/rest/jobstatus/" + + Args: + response: The registered result from the `uri` task (dict-like). + default: Value to return when no ID can be found (if strict=False). + strict: If True, raise AnsibleFilterError when no ID is found. + + Returns: + The job ID string, or `default`/None. + + Raises: + AnsibleFilterError: if `strict=True` and no job ID can be determined. + """ + if not isinstance(response, dict): + if strict: + raise AnsibleFilterError("xwiki_job_id: response must be a dict-like Ansible result.") + return default + + # 1) Try JSON body + j = response.get("json") + if isinstance(j, dict): + job_id = j.get("id") + if isinstance(job_id, dict): + elems = job_id.get("elements") + if isinstance(elems, list) and elems: + return _join_elements(elems) + if isinstance(job_id, str) and job_id.strip(): + return job_id.strip() + + # 2) Fallback: Location header (Ansible `uri` exposes it as `location`) + loc = response.get("location") + if isinstance(loc, str) and loc: + m = _JOB_LOC_RE.search(loc) + if m: + return m.group(1) + + # 3) As a last resort, try the final `url` (in case server redirected and Ansible captured it) + url = response.get("url") + if isinstance(url, str) and url: + m = _JOB_LOC_RE.search(url) + if m: + return m.group(1) + + # Not found + if strict: + raise AnsibleFilterError("xwiki_job_id: could not extract job ID from response.") + return default + + +class FilterModule(object): + """Custom filters for XWiki helpers.""" + def filters(self): + return { + "xwiki_job_id": xwiki_job_id, + } diff --git a/roles/web-app-xwiki/tasks/04_extensions.yml b/roles/web-app-xwiki/tasks/04_extensions.yml index 38c2fe27..685c33e4 100644 --- a/roles/web-app-xwiki/tasks/04_extensions.yml +++ b/roles/web-app-xwiki/tasks/04_extensions.yml @@ -1,55 +1,60 @@ -- name: "Check if OIDC extension installed" - uri: - url: "{{ XWIKI_REST_XWIKI }}/extensions/{{ XWIKI_EXT_OIDC_ID | urlencode }}" - method: GET - user: "{{ XWIKI_SUPERADMIN_USERNAME }}" - password: "{{ XWIKI_SUPERADMIN_PASSWORD }}" - force_basic_auth: yes - status_code: [200, 404, 302] - register: xwiki_oidc_ext - when: XWIKI_OIDC_ENABLED | bool - no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}" +- name: "Probe OIDC extension" + include_tasks: _probe_extension.yml + vars: + ext_id: "{{ XWIKI_EXT_OIDC_ID }}" + ext_enabled: "{{ XWIKI_OIDC_ENABLED }}" + result_var: "xwiki_oidc_ext" -- name: "Check if LDAP extension installed" - uri: - url: "{{ XWIKI_REST_XWIKI }}/extensions/{{ XWIKI_EXT_LDAP_ID | urlencode }}" - method: GET - user: "{{ XWIKI_SUPERADMIN_USERNAME }}" - password: "{{ XWIKI_SUPERADMIN_PASSWORD }}" - force_basic_auth: yes - status_code: [200, 404, 302] - register: xwiki_ldap_ext - when: XWIKI_LDAP_ENABLED | bool - no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}" +- name: "Probe LDAP extension" + include_tasks: _probe_extension.yml + vars: + ext_id: "{{ XWIKI_EXT_LDAP_ID }}" + ext_enabled: "{{ XWIKI_LDAP_ENABLED }}" + result_var: "xwiki_ldap_ext" - name: "Install LDAP and/or OIDC extensions" uri: - url: "{{ XWIKI_REST_EXTENSION_INSTALL }}?jobType=install&async=false" + url: "{{ XWIKI_REST_EXTENSION_INSTALL }}?jobType=install&async=false&media=json" method: PUT user: "{{ XWIKI_SUPERADMIN_USERNAME }}" password: "{{ XWIKI_SUPERADMIN_PASSWORD }}" - force_basic_auth: yes + force_basic_auth: true headers: Content-Type: "text/xml" - Accept: "application/xml" + Accept: "application/json" X-Requested-With: "XMLHttpRequest" body: "{{ lookup('template', 'installjobrequest.xml.j2') }}" status_code: [200, 202] + return_content: yes when: - - (XWIKI_OIDC_ENABLED | bool and xwiki_oidc_ext.status == 404) or - (XWIKI_LDAP_ENABLED | bool and (xwiki_ldap_ext is not skipped) and xwiki_ldap_ext.status == 404) + - (XWIKI_OIDC_ENABLED | bool and (xwiki_oidc_ext.status | default(404)) != 200) + or + (XWIKI_LDAP_ENABLED | bool and (xwiki_ldap_ext is not skipped) and (xwiki_ldap_ext.status | default(404)) != 200) + no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}" + register: xwiki_install_job + +- name: "Extract install job id" + set_fact: + xwiki_install_job_id: "{{ xwiki_install_job | xwiki_job_id(default='') }}" no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}" -- name: "Verify OIDC extension is installed" - when: XWIKI_OIDC_ENABLED | bool +- name: "Poll install job status until FINISHED without errors" + when: xwiki_install_job_id is defined and xwiki_install_job_id|length > 0 uri: - url: "{{ XWIKI_REST_XWIKI }}/extensions/{{ XWIKI_EXT_OIDC_ID | urlencode }}" + url: "{{ XWIKI_REST_BASE }}jobstatus/{{ xwiki_install_job_id }}?media=json" method: GET user: "{{ XWIKI_SUPERADMIN_USERNAME }}" password: "{{ XWIKI_SUPERADMIN_PASSWORD }}" - force_basic_auth: yes + force_basic_auth: true + headers: + Accept: "application/json" status_code: 200 - register: _oidc_ok - retries: 12 - delay: 5 - until: _oidc_ok is succeeded + return_content: yes + register: _install_status + changed_when: false + retries: 20 + delay: 3 + until: + - _install_status is succeeded + - _install_status.json.state == 'FINISHED' + - (_install_status.json.errors | default([]) | length) == 0 diff --git a/roles/web-app-xwiki/tasks/_probe_extension.yml b/roles/web-app-xwiki/tasks/_probe_extension.yml new file mode 100644 index 00000000..b6926a8b --- /dev/null +++ b/roles/web-app-xwiki/tasks/_probe_extension.yml @@ -0,0 +1,22 @@ +- name: "XWIKI | Probe extension {{ ext_id }}" + when: ext_enabled | bool + uri: + url: "{{ XWIKI_REST_XWIKI }}/extensions/{{ ext_id | urlencode }}" + method: GET + user: "{{ XWIKI_SUPERADMIN_USERNAME }}" + password: "{{ XWIKI_SUPERADMIN_PASSWORD }}" + force_basic_auth: true + follow_redirects: none + return_content: no + headers: + Accept: "application/xml" + status_code: [200, 401, 404, 302] + register: _probe + no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}" + changed_when: false + +- name: "XWIKI | Save probe result for {{ ext_id }}" + when: ext_enabled | bool + set_fact: + "{{ result_var }}": "{{ _probe }}" + no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"