diff --git a/group_vars/all/12_oidc.yml b/group_vars/all/12_oidc.yml index 59dbdd24..de0e5a24 100644 --- a/group_vars/all/12_oidc.yml +++ b/group_vars/all/12_oidc.yml @@ -7,33 +7,40 @@ ############################################# # @see https://en.wikipedia.org/wiki/OpenID_Connect -## Helper Variables: -_oidc_client_realm: "{{ OIDC.CLIENT.REALM if OIDC.CLIENT is defined and OIDC.CLIENT.REALM is defined else SOFTWARE_NAME | lower }}" -_oidc_url: "{{ - ( OIDC.URL - if (OIDC is defined and OIDC.URL is defined) - else WEB_PROTOCOL ~ '://' ~ (domains | get_domain('web-app-keycloak')) - ).rstrip('/') - }}" -_oidc_client_issuer_url: "{{ _oidc_url ~ '/realms/' ~ _oidc_client_realm }}" -_oidc_client_id: "{{ OIDC.CLIENT.ID if OIDC.CLIENT is defined and OIDC.CLIENT.ID is defined else SOFTWARE_NAME | lower }}" +# Helper Variables: +_oidc_client_realm: "{{ OIDC.CLIENT.REALM if OIDC.CLIENT is defined and OIDC.CLIENT.REALM is defined else SOFTWARE_NAME | lower }}" +_oidc_url: "{{ + ( OIDC.URL + if (OIDC is defined and OIDC.URL is defined) + else domains | get_url('web-app-keycloak', WEB_PROTOCOL) + ).rstrip('/') + }}" +_oidc_client_issuer_url: "{{ _oidc_url ~ '/realms/' ~ _oidc_client_realm }}" +_oidc_client_id: "{{ OIDC.CLIENT.ID if OIDC.CLIENT is defined and OIDC.CLIENT.ID is defined else SOFTWARE_NAME | lower }}" +_oidc_account_url: "{{ _oidc_client_issuer_url ~ '/account' }}" +_oidc_protocol_oidc: "{{ _oidc_client_issuer_url ~ '/protocol/openid-connect' }}" +# Definition defaults_oidc: URL: "{{ _oidc_url }}" CLIENT: - ID: "{{ _oidc_client_id }}" # Client identifier, typically matching your primary domain -# secret: # Client secret for authenticating with the OIDC provider (set in the inventory file). Recommend greater then 32 characters - REALM: "{{ _oidc_client_realm }}" # The realm to which the client belongs in the OIDC provider - ISSUER_URL: "{{ _oidc_client_issuer_url }}" # Base URL of the OIDC provider (issuer) - DISCOVERY_DOCUMENT: "{{ _oidc_client_issuer_url ~ '/.well-known/openid-configuration' }}" # URL for fetching the provider's configuration details - AUTHORIZE_URL: "{{ _oidc_client_issuer_url ~ '/protocol/openid-connect/auth' }}" # Endpoint to start the authorization process - TOKEN_URL: "{{ _oidc_client_issuer_url ~ '/protocol/openid-connect/token' }}" # Endpoint to exchange authorization codes for tokens (note: 'token_url' may be a typo for 'token_url') - USER_INFO_URL: "{{ _oidc_client_issuer_url ~ '/protocol/openid-connect/userinfo' }}" # Endpoint to retrieve user information - LOGOUT_URL: "{{ _oidc_client_issuer_url ~ '/protocol/openid-connect/logout' }}" # Endpoint to log out the user - CHANGE_CREDENTIALS: "{{ _oidc_client_issuer_url ~ '/account/account-security/signing-in' }}" # URL for managing or changing user credentials - CERTS: "{{ _oidc_client_issuer_url ~ '/protocol/openid-connect/certs' }}" # JSON Web Key Set (JWKS) + ID: "{{ _oidc_client_id }}" # Client identifier, typically matching your primary domain +# secret: # Client secret for authenticating with the OIDC provider (set in the inventory file). Recommend greater then 32 characters + REALM: "{{ _oidc_client_realm }}" # The realm to which the client belongs in the OIDC provider + ISSUER_URL: "{{ _oidc_client_issuer_url }}" # Base URL of the OIDC provider (issuer) + DISCOVERY_DOCUMENT: "{{ _oidc_client_issuer_url ~ '/.well-known/openid-configuration' }}" # URL for fetching the provider's configuration details + AUTHORIZE_URL: "{{ _oidc_protocol_oidc ~ '/auth' }}" # Endpoint to start the authorization process + TOKEN_URL: "{{ _oidc_protocol_oidc ~ '/token' }}" # Endpoint to exchange authorization codes for tokens (note: 'token_url' may be a typo for 'token_url') + USER_INFO_URL: "{{ _oidc_protocol_oidc ~ '/userinfo' }}" # Endpoint to retrieve user information + LOGOUT_URL: "{{ _oidc_protocol_oidc ~ '/logout' }}" # Endpoint to log out the user + CERTS: "{{ _oidc_protocol_oidc ~ '/certs' }}" # JSON Web Key Set (JWKS) + ACCOUNT: + URL: "{{ _oidc_account_url }}" # Entry point for the user settings console + PROFILE_URL: "{{ _oidc_account_url ~ '/#/personal-info' }}" # Section for managing personal information + SECURITY_URL: "{{ _oidc_account_url ~ '/#/security/signingin' }}" # Section for managing login and security settings + CHANGE_CREDENTIALS: "{{ _oidc_account_url ~ '/account-security/signing-in' }}" # URL for managing or changing user credentials RESET_CREDENTIALS: "{{ _oidc_client_issuer_url ~ '/login-actions/reset-credentials?client_id=' ~ _oidc_client_id }}" # Password reset url - BUTTON_TEXT: "SSO Login ({{ PRIMARY_DOMAIN | upper }})" # Default button text + BUTTON_TEXT: "SSO Login ({{ PRIMARY_DOMAIN | upper }})" # Default button text ATTRIBUTES: # Attribut to identify the user USERNAME: "preferred_username" diff --git a/roles/desk-libreoffice/tasks/main.yml b/roles/desk-libreoffice/tasks/main.yml index 97579759..28b9fbf1 100644 --- a/roles/desk-libreoffice/tasks/main.yml +++ b/roles/desk-libreoffice/tasks/main.yml @@ -9,4 +9,4 @@ community.general.pacman: name: "libreoffice-{{ applications['desk-libreoffice'].flavor }}-{{ item }}" state: present - loop: "{{libreoffice_languages}}" + loop: "{{ libreoffice_languages }}" diff --git a/roles/srv-core/templates/nginx.conf.j2 b/roles/srv-core/templates/nginx.conf.j2 index 926d0fa5..1a2626ba 100644 --- a/roles/srv-core/templates/nginx.conf.j2 +++ b/roles/srv-core/templates/nginx.conf.j2 @@ -59,5 +59,5 @@ http # For port proxies stream{ - include {{NGINX.DIRECTORIES.STREAMS}}*.conf; + include {{ NGINX.DIRECTORIES.STREAMS }}*.conf; } diff --git a/roles/svc-db-openldap/tasks/main.yml b/roles/svc-db-openldap/tasks/main.yml index ce8e5aa8..8557c307 100644 --- a/roles/svc-db-openldap/tasks/main.yml +++ b/roles/svc-db-openldap/tasks/main.yml @@ -7,7 +7,7 @@ - name: Create {{ domains | get_domain(application_id) }}.conf if LDAP is exposed to internet template: src: "nginx.stream.conf.j2" - dest: "{{NGINX.DIRECTORIES.STREAMS}}{{ domains | get_domain(application_id) }}.conf" + dest: "{{ NGINX.DIRECTORIES.STREAMS }}{{ domains | get_domain(application_id) }}.conf" notify: restart openresty when: applications | get_app_conf(application_id, 'network.public', True) | bool diff --git a/roles/sys-ctl-hlth-webserver/tasks/01_core.yml b/roles/sys-ctl-hlth-webserver/tasks/01_core.yml index 90e4b7f8..cf1b7127 100644 --- a/roles/sys-ctl-hlth-webserver/tasks/01_core.yml +++ b/roles/sys-ctl-hlth-webserver/tasks/01_core.yml @@ -10,11 +10,12 @@ name: python-requests state: present -- meta: flush_handlers +- name: "Flush webserver handlers" + meta: flush_handlers - include_role: name: sys-service vars: - system_service_on_calendar: "{{ SYS_SCHEDULE_HEALTH_NGINX }}" - system_service_timer_enabled: true - system_service_tpl_on_failure: "{{ SYS_SERVICE_ON_FAILURE_COMPOSE }}" + system_service_on_calendar: "{{ SYS_SCHEDULE_HEALTH_NGINX }}" + system_service_timer_enabled: true + system_service_tpl_on_failure: "{{ SYS_SERVICE_ON_FAILURE_COMPOSE }}" diff --git a/roles/sys-ctl-hlth-webserver/templates/script.py.j2 b/roles/sys-ctl-hlth-webserver/templates/script.py.j2 index 66408bdc..7b570446 100644 --- a/roles/sys-ctl-hlth-webserver/templates/script.py.j2 +++ b/roles/sys-ctl-hlth-webserver/templates/script.py.j2 @@ -15,12 +15,12 @@ def get_expected_statuses(domain: str, parts: list[str], redirected_domains: set Returns: A list of expected HTTP status codes. """ - if domain == '{{domains | get_domain('web-app-listmonk')}}': + if domain == '{{ domains | get_domain('web-app-listmonk') }}': return [404] if (parts and parts[0] == 'www') or (domain in redirected_domains): return [301] - if domain == '{{domains | get_domain('web-app-yourls')}}': - return [{{ applications | get_app_conf('web-app-yourls', 'server.status_codes.landingpage', True) }}] + if domain == '{{ domains | get_domain('web-app-yourls') }}': + return [{{ applications | get_app_conf('web-app-yourls', 'server.status_codes.landingpage') }}] return [200, 302, 301] # file in which fqdn server configs are deposit diff --git a/roles/sys-service/tasks/01_core.yml b/roles/sys-service/tasks/01_core.yml index 5fd5497f..84066247 100644 --- a/roles/sys-service/tasks/01_core.yml +++ b/roles/sys-service/tasks/01_core.yml @@ -1,7 +1,6 @@ - name: Include dependency 'sys-daemon' include_role: name: sys-daemon - public: true when: run_once_sys_daemon is not defined - name: "reset (if enabled)" diff --git a/roles/sys-srv-web-inj-compose/filter_plugins/inj_snippets.py b/roles/sys-srv-web-inj-compose/filter_plugins/inj_snippets.py index df3cac27..424cdf8f 100644 --- a/roles/sys-srv-web-inj-compose/filter_plugins/inj_snippets.py +++ b/roles/sys-srv-web-inj-compose/filter_plugins/inj_snippets.py @@ -1,4 +1,3 @@ -# roles/sys-srv-web-inj-compose/filter_plugins/inj_snippets.py """ Jinja filter: `inj_features(kind)` filters a list of features to only those that actually provide the corresponding snippet template file. diff --git a/roles/sys-srv-web-inj-compose/templates/location.lua.j2 b/roles/sys-srv-web-inj-compose/templates/location.lua.j2 index 795c27c6..d0dcfa5c 100644 --- a/roles/sys-srv-web-inj-compose/templates/location.lua.j2 +++ b/roles/sys-srv-web-inj-compose/templates/location.lua.j2 @@ -1,4 +1,4 @@ -{# roles/sys-srv-web-inj-compose/templates/location.lua.j2 #} +{# Jinja macro: expands feature snippets into Lua array pushes at render time #} {% macro push_snippets(list_name, features) -%} {% set kind = list_name | regex_replace('_snippets$','') %} {% for f in features if inj_enabled.get(f) -%} @@ -14,18 +14,20 @@ header_filter_by_lua_block { local ct = ngx.header.content_type or "" if ct:lower():find("^text/html") then ngx.ctx.is_html = true + -- IMPORTANT: body will be modified → drop Content-Length to avoid mismatches + ngx.header.content_length = nil else ngx.ctx.is_html = false end } body_filter_by_lua_block { - -- only apply further processing if this is an HTML response + -- Only process HTML responses if not ngx.ctx.is_html then return end - -- initialize or reuse the buffer + -- Buffer all chunks until EOF ngx.ctx.buf = ngx.ctx.buf or {} local chunk, eof = ngx.arg[1], ngx.arg[2] @@ -34,39 +36,56 @@ body_filter_by_lua_block { end if not eof then - -- drop intermediate chunks; we’ll emit only on eof + -- Swallow intermediate chunks; emit once at EOF ngx.arg[1] = nil return end - -- on eof: concatenate all buffered chunks + -- Concatenate the full HTML local whole = table.concat(ngx.ctx.buf) - ngx.ctx.buf = nil -- clear buffer + ngx.ctx.buf = nil - -- remove html CSP, due to management via Infinito.Nexus policies - whole = whole:gsub( - ']-http%-equiv=["\']Content%-Security%-Policy["\'][^>]->%s*', - '' - ) + -- Remove inline CSP (case-insensitive) + local meta_re = [[]+http-equiv=["']Content-Security-Policy["'][^>]*>\s*]] + whole = ngx.re.gsub(whole, meta_re, "", "ijo") - -- build a list of head-injection snippets + -- Build head snippets (rendered by Jinja at template time) local head_snippets = {} - {{ push_snippets('head_snippets', inj_head_features) }} - - -- inject all collected snippets right before local head_payload = table.concat(head_snippets, "\n") .. "" - whole = ngx.re.gsub(whole, "", head_payload, "ijo", nil, 1) - -- build a list of body-injection snippets + -- Inject before (first occurrence) + local function repl_head(_) return head_payload end + local new, n, err = ngx.re.sub(whole, [[]], repl_head, "ijo") + if new then + whole = new + else + ngx.log(ngx.WARN, "No found; trying fallback: ", err or "nil") + -- Fallback: inject right AFTER the opening tag + local body_open_re = [[]*>]] + new, n, err = ngx.re.sub(whole, body_open_re, "$0\n" .. table.concat(head_snippets, "\n"), "ijo") + if new then + whole = new + else + ngx.log(ngx.ERR, "Head-fallback failed: ", err or "nil") + end + end + + -- Build body snippets (rendered by Jinja at template time) local body_snippets = {} - {{ push_snippets('body_snippets', inj_body_features) }} - - -- inject all collected snippets right before local body_payload = table.concat(body_snippets, "\n") .. "" - whole = ngx.re.gsub(whole, "", body_payload, "ijo", nil, 1) - -- finally send the modified HTML out - ngx.arg[1] = whole + -- Inject before (first occurrence), or append if missing + local function repl_body(_) return body_payload end + new, n, err = ngx.re.sub(whole, [[]], repl_body, "ijo") + if new then + whole = new + else + ngx.log(ngx.WARN, "No found; appending body snippets at end: ", err or "nil") + whole = whole .. table.concat(body_snippets, "\n") + end + + -- Emit the modified HTML + ngx.arg[1] = whole or "" } diff --git a/roles/sys-srv-web-inj-css/vars/main.yml b/roles/sys-srv-web-inj-css/vars/main.yml index 125b53a2..10d51cb4 100644 --- a/roles/sys-srv-web-inj-css/vars/main.yml +++ b/roles/sys-srv-web-inj-css/vars/main.yml @@ -1,4 +1,4 @@ -global_css_destination: "{{ NGINX.DIRECTORIES.DATA.CDN }}global.css" +global_css_destination: "{{ NGINX.DIRECTORIES.DATA.CDN }}global.css" global_css_base_color: "{{ design.css.colors.base }}" -global_css_count: 7 +global_css_count: 7 global_css_shades: 100 \ No newline at end of file diff --git a/roles/web-app-desktop/config/main.yml b/roles/web-app-desktop/config/main.yml index 2a7cbd38..68788d80 100644 --- a/roles/web-app-desktop/config/main.yml +++ b/roles/web-app-desktop/config/main.yml @@ -2,9 +2,10 @@ features: matomo: true css: true desktop: false - simpleicons: true # Activate Brand Icons for your groups - javascript: true # Necessary for URL sync - logout: false # Doesn't have own user data. Just a frame. + oidc: true # Needs to be activated so that the login url is working + simpleicons: true # Activate Brand Icons for your groups + javascript: true # Necessary for URL sync + logout: true server: csp: whitelist: @@ -19,6 +20,7 @@ server: - https://cdn.jsdelivr.net connect-src: - https://ka-f.fontawesome.com + - "{{ WEB_PROTOCOL }}://auth.{{ PRIMARY_DOMAIN }}" frame-src: - "{{ WEB_PROTOCOL }}://*.{{ PRIMARY_DOMAIN }}" flags: @@ -31,4 +33,8 @@ server: domains: canonical: - "{{ PRIMARY_DOMAIN }}" - +docker: + services: + desktop: + name: "desktop" + image: "application-portfolio" \ No newline at end of file diff --git a/roles/web-app-desktop/tasks/01_core.yml b/roles/web-app-desktop/tasks/01_core.yml index 0eedfebc..bd51b5bf 100644 --- a/roles/web-app-desktop/tasks/01_core.yml +++ b/roles/web-app-desktop/tasks/01_core.yml @@ -2,13 +2,16 @@ include_tasks: "02_validate.yml" when: MODE_ASSERT | bool +- name: "Include JS routines" + include_tasks: "03_javascript.yml" + - name: "load docker, proxy for '{{ application_id }}'" include_role: name: cmp-docker-proxy -- name: "Check if host-specific config.yaml exists in {{ config_inventory_path }}" +- name: "Check if host-specific config.yaml exists in {{ DESKTOP_INVENTORY_CONFIG_PATH }}" stat: - path: "{{ config_inventory_path }}" + path: "{{ DESKTOP_INVENTORY_CONFIG_PATH }}" delegate_to: localhost become: false register: config_file @@ -42,20 +45,20 @@ - name: Copy host-specific config.yaml if it exists template: - src: "{{ config_inventory_path }}" - dest: "{{docker_repository_path}}/app/config.yaml" + src: "{{ DESKTOP_INVENTORY_CONFIG_PATH }}" + dest: "{{ docker_repository_path }}/app/config.yaml" notify: docker compose up when: config_file.stat.exists - name: Copy default config.yaml from the role template if host-specific file does not exist template: src: "config.yaml.j2" - dest: "{{docker_repository_path}}/app/config.yaml" + dest: "{{ docker_repository_path }}/app/config.yaml" notify: docker compose up when: not config_file.stat.exists - name: add docker-compose.yml template: src: docker-compose.yml.j2 - dest: "{docker_compose.directories.instance}}docker-compose.yml" - notify: docker compose up \ No newline at end of file + dest: "{{ docker_compose.directories.instance }}docker-compose.yml" + notify: docker compose up diff --git a/roles/web-app-desktop/tasks/03_javascript.yml b/roles/web-app-desktop/tasks/03_javascript.yml new file mode 100644 index 00000000..d18d238d --- /dev/null +++ b/roles/web-app-desktop/tasks/03_javascript.yml @@ -0,0 +1,19 @@ +- name: "load required 'web-svc-cdn' for {{ application_id }}" + include_role: + name: web-svc-cdn + public: false + when: run_once_web_svc_cdn is not defined + +- name: Ensure {{ DESKTOP_JS_SERVER_DIR }} exists + file: + path: "{{ DESKTOP_JS_SERVER_DIR }}" + state: directory + owner: "{{ NGINX.USER }}" + group: "{{ NGINX.USER }}" + mode: '0755' + +- name: "Include file specific JS Routines" + include_tasks: "_javascript_file.yml" + loop: "{{ DESKTOP_JS_FILES }}" + loop_control: + loop_var: js_file_name \ No newline at end of file diff --git a/roles/web-app-desktop/tasks/_javascript_file.yml b/roles/web-app-desktop/tasks/_javascript_file.yml new file mode 100644 index 00000000..8a452815 --- /dev/null +++ b/roles/web-app-desktop/tasks/_javascript_file.yml @@ -0,0 +1,17 @@ +- name: Deploy {{ js_file_name }} + template: + src: "javascript/{{ js_file_name }}.j2" + dest: "{{ DESKTOP_JS_SERVER_DIR }}/{{ js_file_name }}" + owner: "{{ NGINX.USER }}" + group: "{{ NGINX.USER }}" + mode: '0644' + +- name: Get stat for {{ js_file_name }} + stat: + path: "{{ DESKTOP_JS_SERVER_DIR }}/{{ js_file_name }}" + register: javascript_file_stat + +- name: Update javascript_file_version with highest mtime + set_fact: + javascript_file_version: >- + {{ [ (javascript_file_version | default(0) | int), (javascript_file_stat.stat.mtime | int) ] | max }} diff --git a/roles/web-app-desktop/templates/docker-compose.yml.j2 b/roles/web-app-desktop/templates/docker-compose.yml.j2 index a186fb40..da226fc0 100644 --- a/roles/web-app-desktop/templates/docker-compose.yml.j2 +++ b/roles/web-app-desktop/templates/docker-compose.yml.j2 @@ -2,15 +2,15 @@ portfolio: {% set container_port = 5000 %} build: - context: {{docker_repository_path}} + context: {{ docker_repository_path }} dockerfile: Dockerfile - image: application-portfolio - container_name: portfolio + image: {{ DESKTOP_IMAGE }} + container_name: {{ DESKTOP_CONTAINER }} ports: - 127.0.0.1:{{ ports.localhost.http[application_id] }}:{{ container_port }} volumes: - - {{docker_repository_path}}app:/app - restart: unless-stopped + - {{ docker_repository_path }}app:/app + restart: {{ DOCKER_RESTART_POLICY }} {% include 'roles/docker-container/templates/networks.yml.j2' %} {% include 'roles/docker-container/templates/healthcheck/tcp.yml.j2' %} diff --git a/roles/web-app-desktop/templates/javascript.js.j2 b/roles/web-app-desktop/templates/javascript.js.j2 index b64b08c7..db4459dc 100644 --- a/roles/web-app-desktop/templates/javascript.js.j2 +++ b/roles/web-app-desktop/templates/javascript.js.j2 @@ -1,30 +1,46 @@ -window.addEventListener("message", function(event) { - const allowedSuffix = ".{{ PRIMARY_DOMAIN }}"; - const origin = event.origin; +// ===== Runtime loader for external JS files (no Jinja includes) ===== +(function () { + // 1) Values injected by Ansible/Jinja + // Base URL where your files were deployed (e.g. CDN), made safe w/o trailing slash + const BASE_URL = ("{{ DESKTOP_JS_BASE_URL }}").replace(/\/+$/, ""); + // List of files to load, in order + const FILES = [ + {% for f in DESKTOP_JS_FILES -%} + "{{ f }}"{% if not loop.last %},{% endif %} + {%- endfor %} + ]; + // Cache buster (highest mtime computed during deploy) + const VERSION = "{{ javascript_file_version }}"; - // 1. Only allow messages from *.{{ PRIMARY_DOMAIN }} - if (!origin.endsWith(allowedSuffix)) return; + // 2) Helper to load a