diff --git a/roles/web-app-xwiki/config/main.yml b/roles/web-app-xwiki/config/main.yml index 2b374839..273f241b 100644 --- a/roles/web-app-xwiki/config/main.yml +++ b/roles/web-app-xwiki/config/main.yml @@ -39,3 +39,25 @@ rbac: roles: {} ldap: local_enabled: true # Allows local login if LDAP is down + +plugins: + oidc: + enabled: true + items: + - id: "org.xwiki.contrib.oidc:oidc-authenticator" + version: "2.19.2" +# - id: "org.xwiki.contrib.oidc:oidc-authenticator-ui" +# version: "" # let EM choose a compatible version + + ldap: + enabled: false + items: + - id: "org.xwiki.contrib.ldap:ldap-authenticator" + version: "9.15.7" + + # Example for Matomo + matomo: + enabled: false + items: + - id: "org.xwiki.contrib:matomo" + version: "1.0" diff --git a/roles/web-app-xwiki/files/extension_installer_b64.groovy b/roles/web-app-xwiki/files/extension_installer_b64.groovy new file mode 100644 index 00000000..78f55e58 --- /dev/null +++ b/roles/web-app-xwiki/files/extension_installer_b64.groovy @@ -0,0 +1,43 @@ +// Reads Base64 JSON from placeholder and avoids any quoting issues. +import groovy.json.JsonSlurper +import java.util.Base64 + +def ext = services.extension +def ns = "wiki:xwiki" + +def b64 = '__WANTED_B64__' +def json = new String(Base64.decoder.decode(b64), 'UTF-8') +def wanted = new JsonSlurper().parseText(json) as List + +if (!wanted || wanted.isEmpty()) { + println "SKIP: no extensions requested" + return +} + +wanted.each { e -> + def id = (e.id ?: "").toString() + def ver = (e.version ?: "").toString().trim() + if (!id) { println "ERROR::::Empty extension id in wanted list"; return } + + def already = ext.getInstalledExtension(id, ns) + if (already) { println "ALREADY_INSTALLED::${id}::${already.id?.version}"; return } + + println "INSTALL_START::${id}::${ver ? ver : 'latest'}" + def job + try { + job = ver ? ext.install(id, ver, ns) : ext.install(id, null, ns) + job?.join() + } catch (Throwable t) { + println "ERROR::${id}::${(t?.message ?: t?.toString())}" + } + + def st = job?.status?.state?.name() + if (st) println "STATE=${st}::${id}" + + def err = job?.status?.error // singular! + if (err) println "ERROR::${id}::${(err?.message ?: err?.toString())}" + + def now = ext.getInstalledExtension(id, ns) + if (now) println "INSTALLED_OK::${id}::${now.id?.version}" + else println "INSTALLED_MISSING::${id}" +} diff --git a/roles/web-app-xwiki/tasks/04_extensions.yml b/roles/web-app-xwiki/tasks/04_extensions.yml index 4b10353a..4ab7d908 100644 --- a/roles/web-app-xwiki/tasks/04_extensions.yml +++ b/roles/web-app-xwiki/tasks/04_extensions.yml @@ -1,5 +1,3 @@ -# roles/web-app-xwiki/tasks/04_extensions.yml -# # Installs OIDC / LDAP using a temporary Groovy page that calls the # Extension Script Service (services.extension.install). # Avoids REST job API and any Namespace class import for portability. @@ -13,57 +11,13 @@ # - We print machine-readable markers so Ansible can assert deterministically. # - We protect XWiki's {{groovy}} wiki macro from Jinja by using {% raw %}…{% endraw %}. -- name: "XWIKI | Build Groovy installer code (no wiki macro delimiters here)" +- name: "XWIKI | Build Groovy installer code from static file (base64 payload)" + vars: + _wanted_b64: "{{ XWIKI_PLUGINS | to_json | b64encode }}" set_fact: - _install_code: | - def ext = services.extension - def ns = "wiki:xwiki" - - // Build the wish list from Ansible vars - def wanted = [] - {% if XWIKI_OIDC_ENABLED | bool %} - wanted << [id: "{{ XWIKI_EXT_OIDC_ID }}", version: "{{ XWIKI_EXT_OIDC_VERSION }}"] - {% endif %} - {% if XWIKI_LDAP_ENABLED | bool %} - wanted << [id: "{{ XWIKI_EXT_LDAP_ID }}", version: "{{ XWIKI_EXT_LDAP_VERSION }}"] - {% endif %} - - if (wanted.isEmpty()) { - println "SKIP: no extensions requested" - } else { - wanted.each { e -> - def already = ext.getInstalledExtension(e.id as String, ns) - if (already) { - println "ALREADY_INSTALLED::${e.id}::${already.id?.version}" - } else { - println "INSTALL_START::${e.id}::${e.version}" - def job = ext.install(e.id as String, e.version as String, ns) - - // Heartbeat until terminal state - long last = System.currentTimeMillis() - while (true) { - def st = job?.status?.state - if (st && st.name() in ["FINISHED","FAILED","CANCELED"]) { - println "STATE=${st.name()}::${e.id}" - break - } - if (System.currentTimeMillis() - last > 2000) { - println "STATE=" + (st?.name() ?: "PENDING") + "::" + e.id - last = System.currentTimeMillis() - } - Thread.sleep(500) - } - - // Verify presence after job completion - def now = ext.getInstalledExtension(e.id as String, ns) - if (now) { - println "INSTALLED_OK::${e.id}::${now.id?.version}" - } else { - println "INSTALLED_MISSING::${e.id}" - } - } - } - } + _install_code: >- + {{ lookup('file', 'roles/web-app-xwiki/files/extension_installer_b64.groovy') + | regex_replace('__WANTED_B64__', _wanted_b64) }} - name: "XWIKI | PUT installer page Main.InstallExtensions" uri: @@ -97,36 +51,12 @@ force_basic_auth: true status_code: [200] return_content: yes - timeout: 300 # allow up to 5 minutes per attempt + timeout: 300 register: _exec_page - retries: 20 # retry up to 20 times - delay: 15 # wait 15 seconds between retries + retries: 20 + delay: 15 until: _exec_page is succeeded -# Assert success: -# - If nothing was requested, allow "SKIP: no extensions requested". -# - For requested OIDC/LDAP, require ALREADY_INSTALLED or INSTALLED_OK. -# - Disallow INSTALLED_MISSING. -- name: "ASSERT | Extension installation markers" - vars: - _c: "{{ _exec_page.content | default('') }}" - _need_oidc: "{{ XWIKI_OIDC_ENABLED | bool }}" - _need_ldap: "{{ XWIKI_LDAP_ENABLED | bool }}" - _ok_oidc: "{{ (_c is search('ALREADY_INSTALLED::' ~ XWIKI_EXT_OIDC_ID)) or (_c is search('INSTALLED_OK::' ~ XWIKI_EXT_OIDC_ID)) }}" - _ok_ldap: "{{ (_c is search('ALREADY_INSTALLED::' ~ XWIKI_EXT_LDAP_ID)) or (_c is search('INSTALLED_OK::' ~ XWIKI_EXT_LDAP_ID)) }}" - _miss_any: "{{ _c is search('INSTALLED_MISSING::') }}" - _skip_all: "{{ _c is search('SKIP: no extensions requested') }}" - assert: - that: - - _miss_any | bool == false - - (_need_oidc and _ok_oidc) or (not _need_oidc) - - (_need_ldap and _ok_ldap) or (not _need_ldap) - - (_need_oidc or _need_ldap) or _skip_all - fail_msg: >- - Extension install did not complete successfully. - Output was: - {{ (_exec_page.content | default('') | regex_replace('\\s+', ' ') | truncate(1000)) }} - - name: "XWIKI | Delete installer page" uri: url: "{{ [XWIKI_REST_XWIKI_PAGES, 'InstallExtensions'] | url_join }}" diff --git a/roles/web-app-xwiki/vars/main.yml b/roles/web-app-xwiki/vars/main.yml index f574f4da..5e599f95 100644 --- a/roles/web-app-xwiki/vars/main.yml +++ b/roles/web-app-xwiki/vars/main.yml @@ -44,12 +44,6 @@ XWIKI_REST_XWIKI: "{{ [XWIKI_REST_BASE, 'wikis/xwiki'] | url XWIKI_REST_XWIKI_PAGES: "{{ [XWIKI_REST_BASE, 'wikis/xwiki/spaces/XWiki/pages'] | url_join }}" XWIKI_REST_EXTENSION_INSTALL: "{{ [XWIKI_REST_BASE, 'jobs'] | url_join }}" -# Extension IDs + Versions (pin versions explicitly) -XWIKI_EXT_LDAP_ID: "org.xwiki.contrib.ldap:ldap-authenticator" -XWIKI_EXT_LDAP_VERSION: "9.15.7" -XWIKI_EXT_OIDC_ID: "org.xwiki.contrib.oidc:oidc-authenticator" -XWIKI_EXT_OIDC_VERSION: "2.19.2" - # LDAP configuration (mapped to LDAP.* context) XWIKI_LDAP_SERVER: "{{ LDAP.SERVER.DOMAIN }}" XWIKI_LDAP_PORT: "{{ LDAP.SERVER.PORT }}" @@ -71,3 +65,11 @@ XWIKI_OIDC_CLIENT_SECRET: "{{ OIDC.CLIENT.SECRET }}" XWIKI_OIDC_SCOPES: "openid email profile {{ RBAC.GROUP.CLAIM }}" XWIKI_OIDC_GROUPS_CLAIM: "{{ RBAC.GROUP.CLAIM }}" XWIKI_OIDC_ADMIN_PROVIDER_GROUP: "{{ [RBAC.GROUP.NAME, XWIKI_ADMIN_GROUP] | path_join }}" + +# Collect enabled plugin items from config/main.yml +XWIKI_PLUGINS: >- + {{ + (applications | get_app_conf(application_id, 'plugins')) + | dict2items | selectattr('value.enabled','equalto', true) + | map(attribute='value.items') | list | sum(start=[]) + }}