From 4b9e7dd3b7dd91c221633bc335959f5aa81529d0 Mon Sep 17 00:00:00 2001 From: Kevin Veen-Birkenbach Date: Tue, 22 Jul 2025 13:14:06 +0200 Subject: [PATCH] Implemented universal logout --- filter_plugins/csp_filters.py | 26 +++-- group_vars/all/06_nginx.yml | 3 +- group_vars/all/09_ports.yml | 1 + group_vars/all/10_networks.yml | 2 + .../templates/vhost/basic.conf.j2 | 3 + roles/web-app-akaunting/config/main.yml | 1 + roles/web-app-attendize/config/main.yml | 5 +- roles/web-app-baserow/config/main.yml | 1 + roles/web-app-bigbluebutton/config/main.yml | 5 +- roles/web-app-bluesky/config/main.yml | 5 +- roles/web-app-collabora/config/main.yml | 4 +- roles/web-app-discourse/config/main.yml | 3 +- roles/web-app-elk/config/main.yml | 3 +- roles/web-app-espocrm/config/main.yml | 5 +- roles/web-app-friendica/config/main.yml | 5 +- roles/web-app-funkwhale/config/main.yml | 5 +- roles/web-app-gitea/config/main.yml | 3 +- roles/web-app-gitlab/config/main.yml | 5 +- roles/web-app-jenkins/config/main.yml | 2 + roles/web-app-joomla/config/main.yml | 5 +- roles/web-app-keycloak/config/main.yml | 4 + roles/web-app-keycloak/meta/main.yml | 4 +- roles/web-app-lam/config/main.yml | 3 +- roles/web-app-libretranslate/config/main.yml | 13 +-- roles/web-app-listmonk/config/main.yml | 3 +- roles/web-app-mailu/config/main.yml | 5 +- roles/web-app-mastodon/config/main.yml | 3 +- roles/web-app-matomo/config/main.yml | 3 +- roles/web-app-matrix/config/main.yml | 3 +- roles/web-app-mediawiki/config/main.yml | 4 +- roles/web-app-mig/config/main.yml | 7 +- roles/web-app-mobilizon/config/main.yml | 3 +- roles/web-app-moodle/config/main.yml | 3 +- roles/web-app-mybb/config/main.yml | 5 +- roles/web-app-navigator/config/main.yml | 8 +- roles/web-app-nextcloud/config/main.yml | 3 +- roles/web-app-oauth2-proxy/config/main.yml | 3 +- roles/web-app-openproject/config/main.yml | 3 +- roles/web-app-peertube/config/main.yml | 3 +- roles/web-app-pgadmin/config/main.yml | 5 +- roles/web-app-phpldapadmin/config/main.yml | 5 +- roles/web-app-phpmyadmin/config/main.yml | 1 + roles/web-app-pixelfed/config/main.yml | 8 +- roles/web-app-port-ui/config/main.yml | 1 + roles/web-app-pretix/config/main.yml | 1 + roles/web-app-roulette-wheel/config/main.yml | 2 + roles/web-app-snipe-it/config/main.yml | 3 +- roles/web-app-sphinx/config/main.yml | 1 + roles/web-app-syncope/config/main.yml | 5 +- roles/web-app-taiga/config/main.yml | 3 +- roles/web-app-wordpress/config/main.yml | 3 +- roles/web-app-xmpp/config/main.yml | 3 + roles/web-app-yourls/config/main.yml | 3 +- roles/web-svc-logout/README.md | 35 +++++++ roles/web-svc-logout/Todo.md | 2 + roles/web-svc-logout/__init__.py | 0 roles/web-svc-logout/config/main.yml | 25 +++++ .../web-svc-logout/filter_plugins/__init__.py | 0 .../filter_plugins/domain_filters.py | 49 +++++++++ roles/web-svc-logout/meta/main.yml | 37 +++++++ roles/web-svc-logout/tasks/main.yml | 18 ++++ .../templates/docker-compose.yml.j2 | 14 +++ roles/web-svc-logout/templates/env.j2 | 14 +++ .../templates/logout-proxy.conf.j2 | 20 ++++ roles/web-svc-logout/vars/main.yml | 15 +++ templates/roles/web-app/config/main.yml.j2 | 3 +- tests/integration/test_universal_logout.py | 44 +++++++++ tests/unit/roles/web-svc-logout/__init__.py | 0 .../web-svc-logout/filter_plugins/__init__.py | 0 .../filter_plugins/test_domain_filters.py | 99 +++++++++++++++++++ 70 files changed, 522 insertions(+), 72 deletions(-) create mode 100644 roles/web-svc-logout/README.md create mode 100644 roles/web-svc-logout/Todo.md create mode 100644 roles/web-svc-logout/__init__.py create mode 100644 roles/web-svc-logout/config/main.yml create mode 100644 roles/web-svc-logout/filter_plugins/__init__.py create mode 100644 roles/web-svc-logout/filter_plugins/domain_filters.py create mode 100644 roles/web-svc-logout/meta/main.yml create mode 100644 roles/web-svc-logout/tasks/main.yml create mode 100644 roles/web-svc-logout/templates/docker-compose.yml.j2 create mode 100644 roles/web-svc-logout/templates/env.j2 create mode 100644 roles/web-svc-logout/templates/logout-proxy.conf.j2 create mode 100644 roles/web-svc-logout/vars/main.yml create mode 100644 tests/integration/test_universal_logout.py create mode 100644 tests/unit/roles/web-svc-logout/__init__.py create mode 100644 tests/unit/roles/web-svc-logout/filter_plugins/__init__.py create mode 100644 tests/unit/roles/web-svc-logout/filter_plugins/test_domain_filters.py diff --git a/filter_plugins/csp_filters.py b/filter_plugins/csp_filters.py index 8b3d4869..41a3cea4 100644 --- a/filter_plugins/csp_filters.py +++ b/filter_plugins/csp_filters.py @@ -122,15 +122,23 @@ class FilterModule(object): tokens.append('https://www.gstatic.com') tokens.append('https://www.google.com') - # Enable loading via ancestors - if ( - self.is_feature_enabled(applications, 'port-ui-desktop', application_id) - and directive == 'frame-ancestors' - ): - domain = domains.get('web-app-port-ui')[0] - sld_tld = ".".join(domain.split(".")[-2:]) # yields "example.com" - tokens.append(f"{sld_tld}") # yields "*.example.com" - + if directive == 'frame-ancestors': + # Enable loading via ancestors + if self.is_feature_enabled(applications, 'port-ui-desktop', application_id): + domain = domains.get('web-app-port-ui')[0] + sld_tld = ".".join(domain.split(".")[-2:]) # yields "example.com" + tokens.append(f"{sld_tld}") # yields "*.example.com" + + if self.is_feature_enabled(applications, 'universal_logout', application_id): + + # Allow logout via cymais logout proxy + domain = domains.get('web-svc-logout')[0] + tokens.append(f"{domain}") + + # Allow logout via keycloak app + domain = domains.get('web-app-keycloak')[0] + tokens.append(f"{domain}") + # whitelist tokens += self.get_csp_whitelist(applications, application_id, directive) diff --git a/group_vars/all/06_nginx.yml b/group_vars/all/06_nginx.yml index 4f507046..a9eaadee 100644 --- a/group_vars/all/06_nginx.yml +++ b/group_vars/all/06_nginx.yml @@ -17,5 +17,4 @@ nginx: cache: general: "/tmp/cache_nginx_general/" # Directory which nginx uses to cache general data image: "/tmp/cache_nginx_image/" # Directory which nginx uses to cache images - user: "http" # Default nginx user in ArchLinux - iframe: true # Allows applications to be loaded in iframe \ No newline at end of file + user: "http" # Default nginx user in ArchLinux \ No newline at end of file diff --git a/group_vars/all/09_ports.yml b/group_vars/all/09_ports.yml index 2b1fcb84..edf41cf7 100644 --- a/group_vars/all/09_ports.yml +++ b/group_vars/all/09_ports.yml @@ -69,6 +69,7 @@ ports: web-app-libretranslate: 8045 web-app-pretix: 8046 web-app-mig: 8047 + web-svc-logout: 8048 web-app-bigbluebutton: 48087 # This port is predefined by bbb. @todo Try to change this to a 8XXX port public: # The following ports should be changed to 22 on the subdomain via stream mapping diff --git a/group_vars/all/10_networks.yml b/group_vars/all/10_networks.yml index 5a1ad57f..39930f81 100644 --- a/group_vars/all/10_networks.yml +++ b/group_vars/all/10_networks.yml @@ -94,6 +94,8 @@ defaults_networks: subnet: 192.168.103.144/28 web-app-mig: subnet: 192.168.103.160/28 + web-svc-logout: + subnet: 192.168.103.176/28 # /24 Networks / 254 Usable Clients web-app-bigbluebutton: diff --git a/roles/srv-proxy-7-4-core/templates/vhost/basic.conf.j2 b/roles/srv-proxy-7-4-core/templates/vhost/basic.conf.j2 index 50047ce0..b2970eb1 100644 --- a/roles/srv-proxy-7-4-core/templates/vhost/basic.conf.j2 +++ b/roles/srv-proxy-7-4-core/templates/vhost/basic.conf.j2 @@ -15,6 +15,9 @@ server {% include 'roles/srv-web-7-7-letsencrypt/templates/ssl_header.j2' %} + {% if applications | get_app_conf(application_id, 'features.universal_logout', False) or domain == primary_domain %} + {% include 'roles/web-svc-logout/templates/logout-proxy.conf.j2' %} + {% endif %} {% if applications | get_app_conf(application_id, 'features.oauth2', False) %} {% set acl = applications | get_app_conf(application_id, 'oauth2_proxy.acl', False, {}) %} diff --git a/roles/web-app-akaunting/config/main.yml b/roles/web-app-akaunting/config/main.yml index 2be2a1d3..13152c30 100644 --- a/roles/web-app-akaunting/config/main.yml +++ b/roles/web-app-akaunting/config/main.yml @@ -7,6 +7,7 @@ features: css: true port-ui-desktop: true central_database: true + universal_logout: true domains: canonical: - "accounting.{{ primary_domain }}" diff --git a/roles/web-app-attendize/config/main.yml b/roles/web-app-attendize/config/main.yml index e575c4aa..8a5f54be 100644 --- a/roles/web-app-attendize/config/main.yml +++ b/roles/web-app-attendize/config/main.yml @@ -4,8 +4,9 @@ image: features: matomo: true css: true - port-ui-desktop: true + port-ui-desktop: true central_database: true + universal_logout: true docker: services: redis: @@ -14,4 +15,4 @@ docker: enabled: true domains: canonical: - - "tickets.{{ primary_domain }}" \ No newline at end of file + - "tickets.{{ primary_domain }}" diff --git a/roles/web-app-baserow/config/main.yml b/roles/web-app-baserow/config/main.yml index 60755f7d..774aa080 100644 --- a/roles/web-app-baserow/config/main.yml +++ b/roles/web-app-baserow/config/main.yml @@ -3,6 +3,7 @@ features: css: true port-ui-desktop: true central_database: true + universal_logout: true docker: services: redis: diff --git a/roles/web-app-bigbluebutton/config/main.yml b/roles/web-app-bigbluebutton/config/main.yml index 574ea6e2..3735c648 100644 --- a/roles/web-app-bigbluebutton/config/main.yml +++ b/roles/web-app-bigbluebutton/config/main.yml @@ -7,11 +7,12 @@ api_suffix: "/bigbluebutton/" features: matomo: true css: true - port-ui-desktop: false # Videos can't open in frame due to iframe restrictions + port-ui-desktop: false # Videos can't open in frame due to iframe restrictions # @todo fix this ldap: false oidc: true central_database: false + universal_logout: true domains: canonical: - "meet.{{ primary_domain }}" @@ -21,4 +22,4 @@ csp: unsafe-inline: true style-src: unsafe-inline: true -credentials: {} \ No newline at end of file +credentials: {} diff --git a/roles/web-app-bluesky/config/main.yml b/roles/web-app-bluesky/config/main.yml index d3b76c92..504267aa 100644 --- a/roles/web-app-bluesky/config/main.yml +++ b/roles/web-app-bluesky/config/main.yml @@ -5,8 +5,9 @@ pds: features: matomo: true css: true - port-ui-desktop: true + port-ui-desktop: true central_database: true + universal_logout: true domains: canonical: web: "bskyweb.{{ primary_domain }}" @@ -14,4 +15,4 @@ domains: docker: services: database: - enabled: true \ No newline at end of file + enabled: true diff --git a/roles/web-app-collabora/config/main.yml b/roles/web-app-collabora/config/main.yml index 85970cfa..7e345045 100644 --- a/roles/web-app-collabora/config/main.yml +++ b/roles/web-app-collabora/config/main.yml @@ -6,4 +6,6 @@ docker: redis: enabled: true database: - enabled: false # May this is wrong. Just set during refactoring \ No newline at end of file + enabled: false # May this is wrong. Just set during refactoring +features: + universal_logout: false # I think collabora is more a service then a app. So no login neccessary Propably it makes sense to rename it ;) diff --git a/roles/web-app-discourse/config/main.yml b/roles/web-app-discourse/config/main.yml index 715facb0..fb91ce5f 100644 --- a/roles/web-app-discourse/config/main.yml +++ b/roles/web-app-discourse/config/main.yml @@ -6,6 +6,7 @@ features: oidc: true central_database: true ldap: false # @todo implement and activate + universal_logout: true csp: flags: style-src: @@ -44,4 +45,4 @@ plugins: discourse-solved: enabled: true discourse-voting: - enabled: true \ No newline at end of file + enabled: true diff --git a/roles/web-app-elk/config/main.yml b/roles/web-app-elk/config/main.yml index 8b137891..d3bcee2a 100644 --- a/roles/web-app-elk/config/main.yml +++ b/roles/web-app-elk/config/main.yml @@ -1 +1,2 @@ - +features: + universal_logout: false # Just deactivated to oppress warnings, elk is anyhow not running diff --git a/roles/web-app-espocrm/config/main.yml b/roles/web-app-espocrm/config/main.yml index 57043f1e..14896a65 100644 --- a/roles/web-app-espocrm/config/main.yml +++ b/roles/web-app-espocrm/config/main.yml @@ -1,10 +1,11 @@ features: matomo: true css: false - port-ui-desktop: true + port-ui-desktop: true ldap: false oidc: true central_database: true + universal_logout: true csp: flags: script-src-elem: @@ -34,4 +35,4 @@ docker: version: "latest" name: "espocrm" volumes: - data: espocrm_data \ No newline at end of file + data: espocrm_data diff --git a/roles/web-app-friendica/config/main.yml b/roles/web-app-friendica/config/main.yml index 97a6001b..d3629420 100644 --- a/roles/web-app-friendica/config/main.yml +++ b/roles/web-app-friendica/config/main.yml @@ -3,11 +3,12 @@ images: features: matomo: true css: false # Temporary deactivated - port-ui-desktop: true + port-ui-desktop: true oidc: false # Implementation doesn't work yet central_database: true ldap: true oauth2: false # No special login side which could be protected, use 2FA of Friendica instead + universal_logout: true domains: canonical: - "social.{{ primary_domain }}" @@ -29,4 +30,4 @@ addons: docker: services: database: - enabled: true \ No newline at end of file + enabled: true diff --git a/roles/web-app-funkwhale/config/main.yml b/roles/web-app-funkwhale/config/main.yml index 9da65e14..c545abd3 100644 --- a/roles/web-app-funkwhale/config/main.yml +++ b/roles/web-app-funkwhale/config/main.yml @@ -15,10 +15,11 @@ docker: features: matomo: true css: false - port-ui-desktop: true + port-ui-desktop: true ldap: true central_database: true oauth2: false # Doesn't make sense to activate it atm, because login is possible on homepage + universal_logout: true domains: canonical: - "audio.{{ primary_domain }}" @@ -37,4 +38,4 @@ oauth2_proxy: port: "80" acl: blacklist: - - "/login" \ No newline at end of file + - "/login" diff --git a/roles/web-app-gitea/config/main.yml b/roles/web-app-gitea/config/main.yml index e329b5db..44ba42d5 100644 --- a/roles/web-app-gitea/config/main.yml +++ b/roles/web-app-gitea/config/main.yml @@ -12,6 +12,7 @@ features: ldap: true oauth2: true oidc: false # Deactivated because users aren't auto-created. + universal_logout: true oauth2_proxy: application: "application" port: "<< defaults_applications[web-app-gitea].docker.services.gitea.port >>" @@ -47,4 +48,4 @@ docker: port: 3000 name: "gitea" volumes: - data: "gitea_data" \ No newline at end of file + data: "gitea_data" diff --git a/roles/web-app-gitlab/config/main.yml b/roles/web-app-gitlab/config/main.yml index 1c1b7048..55a4db03 100644 --- a/roles/web-app-gitlab/config/main.yml +++ b/roles/web-app-gitlab/config/main.yml @@ -1,8 +1,9 @@ features: matomo: true css: true - port-ui-desktop: true + port-ui-desktop: true central_database: true + universal_logout: true docker: services: redis: @@ -13,4 +14,4 @@ docker: image: "gitlab/gitlab-ee" version: "latest" credentials: - initial_root_password: "{{ users.administrator.password }}" \ No newline at end of file + initial_root_password: "{{ users.administrator.password }}" diff --git a/roles/web-app-jenkins/config/main.yml b/roles/web-app-jenkins/config/main.yml index e69de29b..5827a713 100644 --- a/roles/web-app-jenkins/config/main.yml +++ b/roles/web-app-jenkins/config/main.yml @@ -0,0 +1,2 @@ +features: + universal_logout: true # Same like with elk, anyhow not active atm diff --git a/roles/web-app-joomla/config/main.yml b/roles/web-app-joomla/config/main.yml index e84a8ce7..25d93676 100644 --- a/roles/web-app-joomla/config/main.yml +++ b/roles/web-app-joomla/config/main.yml @@ -3,12 +3,13 @@ images: features: matomo: true css: true - port-ui-desktop: true + port-ui-desktop: true central_database: true + universal_logout: true domains: canonical: - "cms.{{ primary_domain }}" docker: services: database: - enabled: true \ No newline at end of file + enabled: true diff --git a/roles/web-app-keycloak/config/main.yml b/roles/web-app-keycloak/config/main.yml index 5d9f18eb..0f74b1d0 100644 --- a/roles/web-app-keycloak/config/main.yml +++ b/roles/web-app-keycloak/config/main.yml @@ -6,6 +6,7 @@ features: ldap: true central_database: true recaptcha: true + universal_logout: true csp: flags: script-src-elem: @@ -14,6 +15,9 @@ csp: unsafe-inline: true style-src: unsafe-inline: true + whitelist: + frame-src: + - "*" # For frontend channel logout it's necessary that iframes can be loaded domains: canonical: - "auth.{{ primary_domain }}" diff --git a/roles/web-app-keycloak/meta/main.yml b/roles/web-app-keycloak/meta/main.yml index fab8bae0..75c74e1b 100644 --- a/roles/web-app-keycloak/meta/main.yml +++ b/roles/web-app-keycloak/meta/main.yml @@ -20,4 +20,6 @@ galaxy_info: logo: class: "fa-solid fa-lock" run_after: - - web-app-matomo \ No newline at end of file + - web-app-matomo +dependencies: + - web-svc-logout \ No newline at end of file diff --git a/roles/web-app-lam/config/main.yml b/roles/web-app-lam/config/main.yml index d109c6d0..e6bb7240 100644 --- a/roles/web-app-lam/config/main.yml +++ b/roles/web-app-lam/config/main.yml @@ -8,10 +8,11 @@ oauth2_proxy: features: matomo: true css: true - port-ui-desktop: true + port-ui-desktop: true ldap: true central_database: false oauth2: false + universal_logout: true csp: flags: style-src: diff --git a/roles/web-app-libretranslate/config/main.yml b/roles/web-app-libretranslate/config/main.yml index 8f2bb568..2a418a36 100644 --- a/roles/web-app-libretranslate/config/main.yml +++ b/roles/web-app-libretranslate/config/main.yml @@ -5,9 +5,9 @@ docker: versions: {} # @todo Move under services services: redis: - enabled: false # Enable Redis + enabled: false # Enable Redis database: - enabled: false # Enable the database + enabled: false # Enable the database features: matomo: true # Enable Matomo Tracking css: true # Enable Global CSS Styling @@ -16,10 +16,11 @@ features: central_database: false # Enable Central Database Network recaptcha: false # Enable ReCaptcha oauth2: false # Enable the OAuth2-Proy - javascript: false # Enables the custom JS in the javascript.js.j2 file -csp: - whitelist: {} # URL's which should be whitelisted - flags: {} # Flags which should be set + javascript: false # Enables the custom JS in the javascript.js.j2 file + universal_logout: false # With this app I assume that it's a service, so should be renamed and logging is unneccessary +csp: + whitelist: {} # URL's which should be whitelisted + flags: {} # Flags which should be set domains: canonical: {} # Urls under which the domain should be directly accessible aliases: [] # Alias redirections to the first element of the canonical domains diff --git a/roles/web-app-listmonk/config/main.yml b/roles/web-app-listmonk/config/main.yml index 52874dad..e5a7d655 100644 --- a/roles/web-app-listmonk/config/main.yml +++ b/roles/web-app-listmonk/config/main.yml @@ -5,6 +5,7 @@ features: port-ui-desktop: true central_database: true oidc: true + universal_logout: true domains: canonical: - "newsletter.{{ primary_domain }}" @@ -18,4 +19,4 @@ docker: backup: no_stop_required: true name: listmonk - port: 9000 \ No newline at end of file + port: 9000 diff --git a/roles/web-app-mailu/config/main.yml b/roles/web-app-mailu/config/main.yml index 7908cfca..564a1db5 100644 --- a/roles/web-app-mailu/config/main.yml +++ b/roles/web-app-mailu/config/main.yml @@ -5,9 +5,10 @@ domain: "{{primary_domain}}" # The main domain fr features: matomo: true css: false - port-ui-desktop: true # Deactivated mailu iframe loading until keycloak supports it + port-ui-desktop: true # Deactivated mailu iframe loading until keycloak supports it oidc: true central_database: false # Deactivate central database for mailu, I don't know why the database deactivation is necessary + universal_logout: true domains: canonical: - "mail.{{ primary_domain }}" @@ -32,4 +33,4 @@ docker: enabled: true mailu: version: "2024.06" # Docker Image Version - name: mailu \ No newline at end of file + name: mailu diff --git a/roles/web-app-mastodon/config/main.yml b/roles/web-app-mastodon/config/main.yml index 16268cab..1100dfa9 100644 --- a/roles/web-app-mastodon/config/main.yml +++ b/roles/web-app-mastodon/config/main.yml @@ -6,6 +6,7 @@ features: port-ui-desktop: true oidc: true central_database: true + universal_logout: true domains: canonical: - "microblog.{{ primary_domain }}" @@ -30,4 +31,4 @@ docker: version: latest name: "mastodon-streaming" volumes: - data: "mastodon_data" \ No newline at end of file + data: "mastodon_data" diff --git a/roles/web-app-matomo/config/main.yml b/roles/web-app-matomo/config/main.yml index 6b72f6c1..3068d355 100644 --- a/roles/web-app-matomo/config/main.yml +++ b/roles/web-app-matomo/config/main.yml @@ -8,6 +8,7 @@ features: port-ui-desktop: false # Didn't work in frame didn't have high priority @todo figure out pcause and solve it central_database: true oauth2: false + universal_logout: true csp: whitelist: script-src-elem: @@ -43,4 +44,4 @@ docker: redis: enabled: false volumes: - data: matomo_data \ No newline at end of file + data: matomo_data diff --git a/roles/web-app-matrix/config/main.yml b/roles/web-app-matrix/config/main.yml index 6a32f4cc..638c23ba 100644 --- a/roles/web-app-matrix/config/main.yml +++ b/roles/web-app-matrix/config/main.yml @@ -23,6 +23,7 @@ features: port-ui-desktop: true oidc: true # Deactivated OIDC due to this issue https://github.com/matrix-org/synapse/issues/10492 central_database: true + universal_logout: true csp: flags: script-src: @@ -52,4 +53,4 @@ plugins: domains: canonical: synapse: "matrix.{{ primary_domain }}" - element: "element.{{ primary_domain }}" \ No newline at end of file + element: "element.{{ primary_domain }}" diff --git a/roles/web-app-mediawiki/config/main.yml b/roles/web-app-mediawiki/config/main.yml index 71315403..d74c9b8e 100644 --- a/roles/web-app-mediawiki/config/main.yml +++ b/roles/web-app-mediawiki/config/main.yml @@ -10,4 +10,6 @@ docker: no_stop_required: true name: mediawiki volumes: - data: mediawiki_data \ No newline at end of file + data: mediawiki_data +features: + universal_logout: true diff --git a/roles/web-app-mig/config/main.yml b/roles/web-app-mig/config/main.yml index ba9f6d24..6b69fcf7 100644 --- a/roles/web-app-mig/config/main.yml +++ b/roles/web-app-mig/config/main.yml @@ -8,6 +8,7 @@ features: matomo: true # activate tracking css: true # use custom cymais stile port-ui-desktop: true # Enable in port-ui + universal_logout: false csp: whitelist: script-src-elem: @@ -24,8 +25,8 @@ csp: - https://cdn.jsdelivr.net connect-src: - https://ka-f.fontawesome.com - #frame-src: - # - "{{ web_protocol }}://*.{{primary_domain}}" + frame-ancestors: + - "*" # No damage if it's used somewhere on other websites, it anyhow looks like art flags: style-src: unsafe-inline: true @@ -34,4 +35,4 @@ domains: - "mig.{{ primary_domain }}" aliases: - "meta-infinite-graph.{{ primary_domain }}" -build_data: true # Enables the building of the meta data which the graph requiers \ No newline at end of file +build_data: true # Enables the building of the meta data which the graph requiers diff --git a/roles/web-app-mobilizon/config/main.yml b/roles/web-app-mobilizon/config/main.yml index d9b8b1ff..57ae74ce 100644 --- a/roles/web-app-mobilizon/config/main.yml +++ b/roles/web-app-mobilizon/config/main.yml @@ -4,6 +4,7 @@ features: oidc: true matomo: true port-ui-desktop: true + universal_logout: true csp: flags: script-src-elem: @@ -22,4 +23,4 @@ docker: mobilizon: image: "docker.io/framasoft/mobilizon" name: "mobilizon" - version: "" \ No newline at end of file + version: "" diff --git a/roles/web-app-moodle/config/main.yml b/roles/web-app-moodle/config/main.yml index ee346c9c..586252a9 100644 --- a/roles/web-app-moodle/config/main.yml +++ b/roles/web-app-moodle/config/main.yml @@ -5,6 +5,7 @@ features: port-ui-desktop: true central_database: true oidc: true + universal_logout: true csp: flags: script-src-elem: @@ -35,4 +36,4 @@ docker: volumes: data: moodle_data code: moodle_code - \ No newline at end of file + diff --git a/roles/web-app-mybb/config/main.yml b/roles/web-app-mybb/config/main.yml index cce85b34..0ea0bbd2 100644 --- a/roles/web-app-mybb/config/main.yml +++ b/roles/web-app-mybb/config/main.yml @@ -2,8 +2,9 @@ features: matomo: true css: true - port-ui-desktop: true + port-ui-desktop: true central_database: true + universal_logout: true docker: services: database: @@ -13,4 +14,4 @@ docker: version: "latest" name: "mybb" volumes: - data: "mybb_data" \ No newline at end of file + data: "mybb_data" diff --git a/roles/web-app-navigator/config/main.yml b/roles/web-app-navigator/config/main.yml index d64cb03c..3b6ec98b 100644 --- a/roles/web-app-navigator/config/main.yml +++ b/roles/web-app-navigator/config/main.yml @@ -1,8 +1,8 @@ features: matomo: true css: true - port-ui-desktop: true - + port-ui-desktop: true + universal_logout: false csp: whitelist: script-src-elem: @@ -14,6 +14,8 @@ csp: - https://cdn.jsdelivr.net font-src: - https://cdnjs.cloudflare.com + frame-src: + - "{{ web_protocol }}://*.{{primary_domain}}" # Makes sense that all of the website content is available in the navigator flags: style-src: unsafe-inline: true @@ -23,4 +25,4 @@ csp: unsafe-inline: true domains: canonical: - - "slides.{{ primary_domain }}" \ No newline at end of file + - "slides.{{ primary_domain }}" diff --git a/roles/web-app-nextcloud/config/main.yml b/roles/web-app-nextcloud/config/main.yml index e5f26d6b..0478ec28 100644 --- a/roles/web-app-nextcloud/config/main.yml +++ b/roles/web-app-nextcloud/config/main.yml @@ -59,6 +59,7 @@ features: ldap: true oidc: true central_database: true + universal_logout: true default_quota: '1000000000' # Quota to assign if no quota is specified in the OIDC response (bytes) legacy_login_mask: enabled: False # If true, then legacy login mask is shown. Otherwise just SSO @@ -258,4 +259,4 @@ plugins: - sociallogin whiteboard: # Nextcloud Whiteboard: provides a collaborative drawing and brainstorming tool (https://apps.nextcloud.com/apps/whiteboard) - enabled: true \ No newline at end of file + enabled: true diff --git a/roles/web-app-oauth2-proxy/config/main.yml b/roles/web-app-oauth2-proxy/config/main.yml index 5ce9ad3f..6d3206f8 100644 --- a/roles/web-app-oauth2-proxy/config/main.yml +++ b/roles/web-app-oauth2-proxy/config/main.yml @@ -4,4 +4,5 @@ allowed_roles: "admin" # Restrict it default to admin r features: matomo: true css: true - port-ui-desktop: false \ No newline at end of file + port-ui-desktop: false + universal_logout: true diff --git a/roles/web-app-openproject/config/main.yml b/roles/web-app-openproject/config/main.yml index 32becf5a..cd512499 100644 --- a/roles/web-app-openproject/config/main.yml +++ b/roles/web-app-openproject/config/main.yml @@ -17,6 +17,7 @@ features: ldap: true central_database: true oauth2: true + universal_logout: true csp: flags: script-src-elem: @@ -51,4 +52,4 @@ docker: version: "" # If need a specific memcached version you have to define it here, otherwise the version from svc-db-memcached will be used volumes: - data: "openproject_data" \ No newline at end of file + data: "openproject_data" diff --git a/roles/web-app-peertube/config/main.yml b/roles/web-app-peertube/config/main.yml index 23461f5e..835f67a9 100644 --- a/roles/web-app-peertube/config/main.yml +++ b/roles/web-app-peertube/config/main.yml @@ -4,6 +4,7 @@ features: port-ui-desktop: true central_database: true oidc: true + universal_logout: true csp: flags: script-src-elem: @@ -37,4 +38,4 @@ docker: backup: no_stop_required: true volumes: - data: peertube_data \ No newline at end of file + data: peertube_data diff --git a/roles/web-app-pgadmin/config/main.yml b/roles/web-app-pgadmin/config/main.yml index 07a64e59..b4db551b 100644 --- a/roles/web-app-pgadmin/config/main.yml +++ b/roles/web-app-pgadmin/config/main.yml @@ -9,9 +9,10 @@ oauth2_proxy: features: matomo: true css: true - port-ui-desktop: true + port-ui-desktop: true central_database: true oauth2: true + universal_logout: true csp: flags: style-src: @@ -24,4 +25,4 @@ csp: docker: services: database: - enabled: true \ No newline at end of file + enabled: true diff --git a/roles/web-app-phpldapadmin/config/main.yml b/roles/web-app-phpldapadmin/config/main.yml index 1499f15f..148eba50 100644 --- a/roles/web-app-phpldapadmin/config/main.yml +++ b/roles/web-app-phpldapadmin/config/main.yml @@ -7,6 +7,7 @@ oauth2_proxy: features: matomo: true css: true - port-ui-desktop: true + port-ui-desktop: true ldap: true - oauth2: true \ No newline at end of file + oauth2: true + universal_logout: true diff --git a/roles/web-app-phpmyadmin/config/main.yml b/roles/web-app-phpmyadmin/config/main.yml index b30dd928..3257e918 100644 --- a/roles/web-app-phpmyadmin/config/main.yml +++ b/roles/web-app-phpmyadmin/config/main.yml @@ -11,6 +11,7 @@ features: # it's anyhow not so enduser relevant, so it can be kept like this central_database: true oauth2: true + universal_logout: true csp: flags: style-src: diff --git a/roles/web-app-pixelfed/config/main.yml b/roles/web-app-pixelfed/config/main.yml index 10d41926..c184cf84 100644 --- a/roles/web-app-pixelfed/config/main.yml +++ b/roles/web-app-pixelfed/config/main.yml @@ -2,9 +2,10 @@ titel: "Pictures on {{primary_domain}}" features: matomo: true css: false # Needs to be reactivated - port-ui-desktop: true + port-ui-desktop: true central_database: true oidc: true + universal_logout: true csp: flags: script-src: @@ -15,6 +16,9 @@ csp: unsafe-eval: true style-src: unsafe-inline: true + whitelist: + frame-ancestors: + - "*" domains: canonical: - "picture.{{ primary_domain }}" @@ -27,7 +31,7 @@ docker: database: enabled: true pixelfed: - image: "zknt/pixelfed" + image: "zknt/pixelfed" version: "latest" name: "pixelfed" backup: diff --git a/roles/web-app-port-ui/config/main.yml b/roles/web-app-port-ui/config/main.yml index e73af010..c737c016 100644 --- a/roles/web-app-port-ui/config/main.yml +++ b/roles/web-app-port-ui/config/main.yml @@ -4,6 +4,7 @@ features: port-ui-desktop: false simpleicons: true # Activate Brand Icons for your groups javascript: true # Necessary for URL sync + universal_logout: false # Doesn't have own user data. Just a frame. csp: whitelist: script-src-elem: diff --git a/roles/web-app-pretix/config/main.yml b/roles/web-app-pretix/config/main.yml index 8f2bb568..9c2a86ed 100644 --- a/roles/web-app-pretix/config/main.yml +++ b/roles/web-app-pretix/config/main.yml @@ -17,6 +17,7 @@ features: recaptcha: false # Enable ReCaptcha oauth2: false # Enable the OAuth2-Proy javascript: false # Enables the custom JS in the javascript.js.j2 file + universal_logout: true csp: whitelist: {} # URL's which should be whitelisted flags: {} # Flags which should be set diff --git a/roles/web-app-roulette-wheel/config/main.yml b/roles/web-app-roulette-wheel/config/main.yml index 0af1d539..55ac2467 100644 --- a/roles/web-app-roulette-wheel/config/main.yml +++ b/roles/web-app-roulette-wheel/config/main.yml @@ -1,3 +1,5 @@ +features: + universal_logout: false domains: canonical: - "wheel.{{ primary_domain }}" diff --git a/roles/web-app-snipe-it/config/main.yml b/roles/web-app-snipe-it/config/main.yml index 957a17c9..a6df9502 100644 --- a/roles/web-app-snipe-it/config/main.yml +++ b/roles/web-app-snipe-it/config/main.yml @@ -5,6 +5,7 @@ features: central_database: true ldap: true oauth2: true + universal_logout: true domains: canonical: - "inventory.{{ primary_domain }}" @@ -38,4 +39,4 @@ docker: image: "grokability/snipe-it" volumes: data: "snipe-it_data" - \ No newline at end of file + diff --git a/roles/web-app-sphinx/config/main.yml b/roles/web-app-sphinx/config/main.yml index 07a28fd9..40a00533 100644 --- a/roles/web-app-sphinx/config/main.yml +++ b/roles/web-app-sphinx/config/main.yml @@ -2,6 +2,7 @@ features: matomo: true css: true port-ui-desktop: true + universal_logout: false csp: flags: script-src: diff --git a/roles/web-app-syncope/config/main.yml b/roles/web-app-syncope/config/main.yml index 5c545de1..7c79d680 100644 --- a/roles/web-app-syncope/config/main.yml +++ b/roles/web-app-syncope/config/main.yml @@ -1,3 +1,6 @@ + +features: + universal_logout: false # Role is not enabled until then keep it false # syncope: # version: "latest" # credentials: @@ -9,4 +12,4 @@ # password: "{{ users.administrator.password }}" # users: # administrator: -# username: "{{ users.administrator.username }}" \ No newline at end of file +# username: "{{ users.administrator.username }}" diff --git a/roles/web-app-taiga/config/main.yml b/roles/web-app-taiga/config/main.yml index 3c8c351d..6e7225c3 100644 --- a/roles/web-app-taiga/config/main.yml +++ b/roles/web-app-taiga/config/main.yml @@ -11,6 +11,7 @@ features: port-ui-desktop: true oidc: false central_database: true + universal_logout: true docker: services: database: @@ -28,4 +29,4 @@ csp: unsafe-eval: true domains: canonical: - - "kanban.{{ primary_domain }}" \ No newline at end of file + - "kanban.{{ primary_domain }}" diff --git a/roles/web-app-wordpress/config/main.yml b/roles/web-app-wordpress/config/main.yml index 3003b510..d8701fa1 100644 --- a/roles/web-app-wordpress/config/main.yml +++ b/roles/web-app-wordpress/config/main.yml @@ -9,9 +9,10 @@ plugins: features: matomo: true css: false - port-ui-desktop: true + port-ui-desktop: true oidc: true central_database: true + universal_logout: true csp: flags: style-src: diff --git a/roles/web-app-xmpp/config/main.yml b/roles/web-app-xmpp/config/main.yml index e69de29b..83751c72 100644 --- a/roles/web-app-xmpp/config/main.yml +++ b/roles/web-app-xmpp/config/main.yml @@ -0,0 +1,3 @@ +# xmpp is more a service then a app with ui interface. @todo Rename it +features: + universal_logout: false # Reactivated as soon as xmpp is fully implemented diff --git a/roles/web-app-yourls/config/main.yml b/roles/web-app-yourls/config/main.yml index 21905cfe..2abeb5fa 100644 --- a/roles/web-app-yourls/config/main.yml +++ b/roles/web-app-yourls/config/main.yml @@ -12,6 +12,7 @@ features: port-ui-desktop: true central_database: true oauth2: true + universal_logout: true domains: canonical: - "s.{{ primary_domain }}" @@ -24,4 +25,4 @@ docker: yourls: version: "latest" name: "yourls" - image: "yourls" \ No newline at end of file + image: "yourls" diff --git a/roles/web-svc-logout/README.md b/roles/web-svc-logout/README.md new file mode 100644 index 00000000..260b02dc --- /dev/null +++ b/roles/web-svc-logout/README.md @@ -0,0 +1,35 @@ +# web-svc-logout + +This folder contains an Ansible role to deploy and configure the **Universal Logout Service**. + +## Description + +This role sets up the universal logout proxy service, a Dockerized Python Flask container that coordinates logout requests across multiple OIDC-integrated applications. It also configures the necessary Nginx proxy snippets and environment variables to enable unified logout flows. + +It solves the common challenge of logging a user out from all connected apps with a single action, especially in environments where apps live on multiple subdomains and use OIDC authentication. + +## Overview + +- Deploys the universal logout service container based on the official [universal-logout GitHub repository](https://github.com/kevinveenbirkenbach/universal-logout). +- Configures the logout domains dynamically based on application inventory and features using custom Ansible filters. +- Provides an Nginx `/logout` proxy configuration snippet that handles CORS and forwards logout requests to the logout service. +- Supplies a user-friendly logout conductor UI that requests logout on all configured domains and shows live status. +- Designed to be used as the Front Channel Logout URL for Keycloak or other OpenID Connect providers, enabling a seamless, service-spanning logout experience. + +## Features + +- Automatic discovery of logout domains from applications with the `features.universal_logout` flag enabled. +- Centralized logout proxy that clears cookies and sessions across all configured subdomains. +- Status page with live feedback on logout progress for each domain. +- Built-in support for Docker Compose deployment and integration with the CyMaIS ecosystem. +- Includes security-conscious headers (CORS, CSP) for smooth cross-domain logout operations. + +## Further Resources + +- [Universal Logout GitHub Repository](https://github.com/kevinveenbirkenbach/universal-logout) +- [CyMaIS Project](https://cymais.cloud) +- [Author: Kevin Veen-Birkenbach](https://veen.world) + +--- + +*This role is licensed under the [CyMaIS NonCommercial License (CNCL)](https://s.veen.world/cncl).* diff --git a/roles/web-svc-logout/Todo.md b/roles/web-svc-logout/Todo.md new file mode 100644 index 00000000..2f46ca25 --- /dev/null +++ b/roles/web-svc-logout/Todo.md @@ -0,0 +1,2 @@ +# Todos +- solve loading of domains which are not in group names, but declared via dependencies \ No newline at end of file diff --git a/roles/web-svc-logout/__init__.py b/roles/web-svc-logout/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/roles/web-svc-logout/config/main.yml b/roles/web-svc-logout/config/main.yml new file mode 100644 index 00000000..9c0b0ff7 --- /dev/null +++ b/roles/web-svc-logout/config/main.yml @@ -0,0 +1,25 @@ +features: + matomo: true + css: true + port-ui-desktop: true + javascript: false +domains: + canonical: + - "logout.{{ primary_domain }}" +csp: + flags: + style-src: + unsafe-inline: true + script-src-elem: + unsafe-inline: true + whitelist: + connect-src: + - "{{ web_protocol }}://*.{{ primary_domain }}" + - "{{ web_protocol }}://{{ primary_domain }}" + script-src-elem: + - https://cdn.jsdelivr.net + style-src: + - https://cdn.jsdelivr.net + frame-ancestors: + - "{{ web_protocol }}://<< defaults_applications[web-app-keycloak].domains.canonical[0] >>" + diff --git a/roles/web-svc-logout/filter_plugins/__init__.py b/roles/web-svc-logout/filter_plugins/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/roles/web-svc-logout/filter_plugins/domain_filters.py b/roles/web-svc-logout/filter_plugins/domain_filters.py new file mode 100644 index 00000000..5daecb46 --- /dev/null +++ b/roles/web-svc-logout/filter_plugins/domain_filters.py @@ -0,0 +1,49 @@ +# roles/web-svc-logout/filter_plugins/domain_filters.py + +from ansible.errors import AnsibleFilterError +import sys, os +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', '..'))) +from module_utils.config_utils import get_app_conf + +class FilterModule(object): + """Ansible filter plugin for generating logout domains based on universal_logout feature.""" + + def filters(self): + return { + 'logout_domains': self.logout_domains, + } + + def logout_domains(self, applications, group_names): + """ + Return a list of domains for applications where features.universal_logout is true. + + :param applications: dict of application configs + :param group_names: list of application IDs to consider + :return: flat list of domain strings + """ + try: + result = [] + for app_id, config in applications.items(): + if app_id not in group_names: + continue + + if not get_app_conf(applications, app_id, 'features.universal_logout', False): + continue + + # use canonical domains list if present + domains_entry = config.get('domains', {}).get('canonical', []) + + # normalize to a list of strings + if isinstance(domains_entry, dict): + flattened = list(domains_entry.values()) + elif isinstance(domains_entry, list): + flattened = domains_entry + else: + flattened = [domains_entry] + + result.extend(flattened) + + return result + + except Exception as e: + raise AnsibleFilterError(f"logout_domains filter error: {e}") diff --git a/roles/web-svc-logout/meta/main.yml b/roles/web-svc-logout/meta/main.yml new file mode 100644 index 00000000..2968b2db --- /dev/null +++ b/roles/web-svc-logout/meta/main.yml @@ -0,0 +1,37 @@ +galaxy_info: + author: "Kevin Veen‑Birkenbach" + description: > + Deploys the universal logout service: a Dockerized Python container, + Nginx `/logout` proxies for `*.cymais.cloud`, and the `conductor.html.j2` + template for unified logout orchestration. + license: "CyMaIS NonCommercial License (CNCL)" + license_url: "https://s.veen.world/cncl" + company: | + Kevin Veen‑Birkenbach + Consulting & Coaching Solutions + https://www.veen.world + min_ansible_version: "2.9" + platforms: + - name: Docker + versions: + - latest + - name: Debian + versions: + - buster + - bullseye + - name: Ubuntu + versions: + - focal + - jammy + galaxy_tags: + - ansible + - docker + - flask + - nginx + - cymais + - logout + repository: "https://github.com/kevinveenbirkenbach/universal-logout" + issue_tracker_url: "https://github.com/kevinveenbirkenbach/universal-logout/issues" + documentation: "https://github.com/kevinveenbirkenbach/universal-logout#readme" + logo: + class: "fa fa-sign-out-alt" diff --git a/roles/web-svc-logout/tasks/main.yml b/roles/web-svc-logout/tasks/main.yml new file mode 100644 index 00000000..057fcca0 --- /dev/null +++ b/roles/web-svc-logout/tasks/main.yml @@ -0,0 +1,18 @@ +--- + +- name: "include docker and reverse proxy for '{{ application_id }}'" + include_role: + name: cmp-docker-proxy + when: run_once_web_svc_logout is not defined + +- name: Create symbolic link from .env file to repository + file: + src: "{{ docker_compose.files.env }}" + dest: "{{ [ docker_repository_path, '.env' ] | path_join }}" + state: link + when: run_once_web_svc_logout is not defined + +- name: run the web svc logout tasks once + set_fact: + run_once_web_svc_logout: true + when: run_once_web_svc_logout is not defined \ No newline at end of file diff --git a/roles/web-svc-logout/templates/docker-compose.yml.j2 b/roles/web-svc-logout/templates/docker-compose.yml.j2 new file mode 100644 index 00000000..494edabe --- /dev/null +++ b/roles/web-svc-logout/templates/docker-compose.yml.j2 @@ -0,0 +1,14 @@ +{% include 'roles/docker-compose/templates/base.yml.j2' %} + logout: + {% include 'roles/docker-container/templates/base.yml.j2' %} + build: + context: {{ docker_repository_path }} + dockerfile: Dockerfile + image: logout + container_name: logout + ports: + - 127.0.0.1:{{ports.localhost.http[application_id]}}:{{ container_port }} +{% include 'roles/docker-container/templates/networks.yml.j2' %} +{% include 'roles/docker-container/templates/healthcheck/tcp.yml.j2' %} + +{% include 'roles/docker-compose/templates/networks.yml.j2' %} \ No newline at end of file diff --git a/roles/web-svc-logout/templates/env.j2 b/roles/web-svc-logout/templates/env.j2 new file mode 100644 index 00000000..51813496 --- /dev/null +++ b/roles/web-svc-logout/templates/env.j2 @@ -0,0 +1,14 @@ +# Comma‑separated list of all subdomains to log out (no spaces) +LOGOUT_DOMAINS={{ logout_domains }} + +# Port the logout service will listen on inside the container +LOGOUT_PORT={{ container_port }} + +# (Optional) If you’re using docker‑compose, you can also define: +#HOST_LOGOUT_PORT=8080 +#HOST_NGINX_HTTP_PORT=80 +#HOST_NGINX_HTTPS_PORT=443 + +# (For the Nginx Jinja2 proxy snippet) +#LOGOUT_SERVICE_HOST=logout-service +#LOGOUT_SERVICE_PORT=8000 diff --git a/roles/web-svc-logout/templates/logout-proxy.conf.j2 b/roles/web-svc-logout/templates/logout-proxy.conf.j2 new file mode 100644 index 00000000..529e8ce9 --- /dev/null +++ b/roles/web-svc-logout/templates/logout-proxy.conf.j2 @@ -0,0 +1,20 @@ +location = /logout { + # Proxy to the logout service + proxy_pass http://127.0.0.1:{{ ports.localhost.http['web-svc-logout'] }}/logout; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_http_version 1.1; + + # CORS headers – allow your central page to call this + add_header 'Access-Control-Allow-Origin' '{{ domains | get_url('web-svc-logout', web_protocol) }}' always; + add_header 'Access-Control-Allow-Credentials' 'true' always; + add_header 'Access-Control-Allow-Methods' 'GET, OPTIONS' always; + add_header 'Access-Control-Allow-Headers' 'Accept, Authorization' always; + + # Handle preflight + if ($request_method = OPTIONS) { + return 204; + } +} \ No newline at end of file diff --git a/roles/web-svc-logout/vars/main.yml b/roles/web-svc-logout/vars/main.yml new file mode 100644 index 00000000..447a7d6f --- /dev/null +++ b/roles/web-svc-logout/vars/main.yml @@ -0,0 +1,15 @@ +application_id: "web-svc-logout" +docker_repository_address: "https://github.com/kevinveenbirkenbach/universal-logout" +docker_pull_git_repository: true +container_port: 8000 + +# The following line leads to that services which arent listed directly in the inventory, +# but are called over other roles, aren't listed here +# @todo implement the calling of also dependency domains (propably the easiest to write a script which adds all dependencies to group_names) +logout_domains: >- + {{ + ( + [primary_domain] + + (applications | logout_domains(group_names)) + ) | unique | join(',') + }} diff --git a/templates/roles/web-app/config/main.yml.j2 b/templates/roles/web-app/config/main.yml.j2 index ef7e7b4a..3c036266 100644 --- a/templates/roles/web-app/config/main.yml.j2 +++ b/templates/roles/web-app/config/main.yml.j2 @@ -23,7 +23,8 @@ features: central_database: false # Enable Central Database Network recaptcha: false # Enable ReCaptcha oauth2: false # Enable the OAuth2-Proy - javascript: false # Enables the custom JS in the javascript.js.j2 file + javascript: false # Enable the custom JS in the javascript.js.j2 file + universal_logout: true # Enable the logout via the central logout mechanism (deleting all cookies) csp: whitelist: # URL's which should be whitelisted script-src-elem: [] diff --git a/tests/integration/test_universal_logout.py b/tests/integration/test_universal_logout.py new file mode 100644 index 00000000..90e3ff06 --- /dev/null +++ b/tests/integration/test_universal_logout.py @@ -0,0 +1,44 @@ +import unittest +import glob +import yaml + +class TestUniversalLogoutSetting(unittest.TestCase): + ROLES_PATH = "roles/web-app-*/config/main.yml" + + def test_universal_logout_defined(self): + files = glob.glob(self.ROLES_PATH) + self.assertGreater(len(files), 0, f"No role config files found under {self.ROLES_PATH}") + + errors = [] + + for file_path in files: + with open(file_path, "r", encoding="utf-8") as f: + try: + data = yaml.safe_load(f) + except yaml.YAMLError as e: + errors.append(f"YAML parse error in '{file_path}': {e}") + continue + + features = {} + if data is not None: + features = data.get("features", {}) + + if "universal_logout" not in features: + errors.append( + f"Missing 'universal_logout' setting in features of '{file_path}'. " + "You must explicitly set 'universal_logout' to true or false for this app." + ) + else: + val = features["universal_logout"] + if not isinstance(val, bool): + errors.append( + f"The 'universal_logout' setting in '{file_path}' must be boolean true or false, " + f"but found: {val} (type {type(val).__name__})" + ) + + if errors: + self.fail("\n\n".join(errors)) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/unit/roles/web-svc-logout/__init__.py b/tests/unit/roles/web-svc-logout/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/unit/roles/web-svc-logout/filter_plugins/__init__.py b/tests/unit/roles/web-svc-logout/filter_plugins/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/unit/roles/web-svc-logout/filter_plugins/test_domain_filters.py b/tests/unit/roles/web-svc-logout/filter_plugins/test_domain_filters.py new file mode 100644 index 00000000..1b41b8d9 --- /dev/null +++ b/tests/unit/roles/web-svc-logout/filter_plugins/test_domain_filters.py @@ -0,0 +1,99 @@ +# tests/unit/roles/web-svc-logout/filter_plugins/test_domain_filters.py + +import os +import unittest +import importlib.util + +# Directory of this test file: .../tests/unit/roles/web-svc-logout/filter_plugins +THIS_DIR = os.path.dirname(__file__) + +# Compute the repo root by going up five levels: tests → unit → roles → web-svc-logout → filter_plugins → repo root +REPO_ROOT = os.path.abspath(os.path.join(THIS_DIR, '../../../../..')) + +# Path to the actual plugin under roles/web-svc-logout/filter_plugins +DOMAIN_FILTERS_PATH = os.path.join( + REPO_ROOT, + 'roles', + 'web-svc-logout', + 'filter_plugins', + 'domain_filters.py' +) + +# Dynamically load the domain_filters module +spec = importlib.util.spec_from_file_location('domain_filters', DOMAIN_FILTERS_PATH) +domain_filters = importlib.util.module_from_spec(spec) +spec.loader.exec_module(domain_filters) +FilterModule = domain_filters.FilterModule + + +class TestLogoutDomainsFilter(unittest.TestCase): + def setUp(self): + self.filter_fn = FilterModule().filters()['logout_domains'] + + def test_flatten_and_feature_flag(self): + applications = { + "app1": { + "domains": {"canonical": "single.domain.com"}, + "features": {"universal_logout": True}, + }, + "app2": { + "domains": {"canonical": ["list1.com", "list2.com"]}, + "features": {"universal_logout": True}, + }, + "app3": { + "domains": {"canonical": {"k1": "dictA.com", "k2": "dictB.com"}}, + "features": {"universal_logout": True}, + }, + "app4": { + "domains": {"canonical": "no-logout.com"}, + "features": {"universal_logout": False}, + }, + "other": { + "domains": {"canonical": "ignored.com"}, + "features": {"universal_logout": True}, + }, + } + group_names = ["app1", "app2", "app3", "app4"] + result = set(self.filter_fn(applications, group_names)) + expected = { + "single.domain.com", + "list1.com", + "list2.com", + "dictA.com", + "dictB.com", + } + self.assertEqual(result, expected) + + def test_missing_canonical_defaults_empty(self): + applications = { + "app1": { + "domains": {}, # no 'canonical' key + "features": {"universal_logout": True}, + } + } + group_names = ["app1"] + self.assertEqual(self.filter_fn(applications, group_names), []) + + def test_app_not_in_group(self): + applications = { + "app1": { + "domains": {"canonical": "domain.com"}, + "features": {"universal_logout": True}, + } + } + group_names = [] + self.assertEqual(self.filter_fn(applications, group_names), []) + + def test_invalid_domain_type(self): + applications = { + "app1": { + "domains": {"canonical": 123}, + "features": {"universal_logout": True}, + } + } + group_names = ["app1"] + self.assertEqual(self.filter_fn(applications, group_names), [123]) + + +if __name__ == '__main__': + unittest.main()