diff --git a/roles/srv-web-7-7-inj-logout/meta/main.yml b/roles/srv-web-7-7-inj-logout/meta/main.yml new file mode 100644 index 00000000..3ea40421 --- /dev/null +++ b/roles/srv-web-7-7-inj-logout/meta/main.yml @@ -0,0 +1,28 @@ +--- +galaxy_info: + author: "Kevin Veen-Birkenbach" + description: "Injects a catcher, which catches the actions of all logout elements and redirects them to the central logout." + company: | + Kevin Veen-Birkenbach + Consulting & Coaching Solutions + https://www.veen.world + license: "CyMaIS NonCommercial License (CNCL)" + license_url: "https://s.veen.world/cncl" + min_ansible_version: "2.9" + platforms: + - name: Archlinux + versions: + - rolling + galaxy_tags: + - nginx + - javascript + - csp + - sub_filter + - injection + - global + repository: "https://s.veen.world/cymais" + documentation: "https://s.veen.world/cymais" + issue_tracker_url: "https://s.veen.world/cymaisissues" + +dependencies: + - srv-web-7-4-core diff --git a/roles/srv-web-7-7-inj-logout/tasks/main.yml b/roles/srv-web-7-7-inj-logout/tasks/main.yml new file mode 100644 index 00000000..39206a1f --- /dev/null +++ b/roles/srv-web-7-7-inj-logout/tasks/main.yml @@ -0,0 +1,13 @@ +# run_once_srv_web_7_7_inj_javascript: deactivated +- name: "Load JavaScript code for '{{ application_id }}'" + set_fact: + javascript_code: "{{ lookup('template', modifier_javascript_template_file) }}" + +- name: "Collapse Javascript code into one-liner for '{{application_id}}'" + set_fact: + javascript_code_one_liner: "{{ javascript_code | to_one_liner }}" + +- name: "Append Javascript CSP hash for '{{application_id}}'" + set_fact: + applications: "{{ applications | append_csp_hash(application_id, javascript_code_one_liner) }}" + changed_when: false diff --git a/roles/srv-web-7-7-inj-logout/templates/head_sub.j2 b/roles/srv-web-7-7-inj-logout/templates/head_sub.j2 new file mode 100644 index 00000000..1731b48b --- /dev/null +++ b/roles/srv-web-7-7-inj-logout/templates/head_sub.j2 @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/roles/srv-web-7-7-inj-logout/templates/logout.js.j2 b/roles/srv-web-7-7-inj-logout/templates/logout.js.j2 new file mode 100644 index 00000000..d8b2fdea --- /dev/null +++ b/roles/srv-web-7-7-inj-logout/templates/logout.js.j2 @@ -0,0 +1,38 @@ +(function() { + const logoutUrlBase = 'https://auth.cymais.cloud/realms/cymais.cloud/protocol/openid-connect/logout'; + const redirectUri = encodeURIComponent('https://cymais.cloud'); + const logoutUrl = `${logoutUrlBase}?redirect_uri=${redirectUri}`; + + // Check if a string matches logout keywords + function matchesLogout(str) { + return str && /logout|log\s*out|abmelden/i.test(str); + } + + // Check if any attribute name contains "logout" (case-insensitive) + function hasLogoutAttribute(el) { + for (let attr of el.attributes) { + if (/logout/i.test(attr.name)) { + return true; + } + } + return false; + } + + // Find all elements + const allElements = document.querySelectorAll('*'); + allElements.forEach(el => { + if ( + matchesLogout(el.getAttribute('name')) || + matchesLogout(el.id) || + matchesLogout(el.className) || + matchesLogout(el.innerText) || + hasLogoutAttribute(el) + ) { + el.style.cursor = 'pointer'; + el.addEventListener('click', function(event) { + event.preventDefault(); + window.location.href = logoutUrl; + }); + } + }); +})(); diff --git a/roles/srv-web-7-7-inj-logout/vars/main.yml b/roles/srv-web-7-7-inj-logout/vars/main.yml new file mode 100644 index 00000000..1e72f9bc --- /dev/null +++ b/roles/srv-web-7-7-inj-logout/vars/main.yml @@ -0,0 +1 @@ +modifier_javascript_template_file: "{{ application_id | abs_role_path_by_application_id }}/templates/javascript.js.j2" \ No newline at end of file diff --git a/tests/integration/test_domain_uniqueness.py b/tests/integration/test_domain_uniqueness.py index ea3a80de..46929090 100644 --- a/tests/integration/test_domain_uniqueness.py +++ b/tests/integration/test_domain_uniqueness.py @@ -2,7 +2,7 @@ import unittest import yaml import subprocess from pathlib import Path -from collections import Counter +from collections import Counter, defaultdict class TestDomainUniqueness(unittest.TestCase): def test_no_duplicate_domains(self): @@ -22,7 +22,8 @@ class TestDomainUniqueness(unittest.TestCase): cfg = yaml.safe_load(yaml_file.read_text(encoding='utf-8')) or {} apps = cfg.get('defaults_applications', {}) - all_domains = [] + domain_to_apps = defaultdict(set) + for app_name, app_cfg in apps.items(): domains_cfg = app_cfg.get('domains', {}) @@ -32,7 +33,10 @@ class TestDomainUniqueness(unittest.TestCase): values = list(canonical.values()) else: values = canonical or [] - all_domains.extend(values) + + for d in values: + if isinstance(d, str) and d.strip(): + domain_to_apps[d].add(app_name) # aliases entries may be a list or a mapping aliases = domains_cfg.get('aliases', []) @@ -40,16 +44,16 @@ class TestDomainUniqueness(unittest.TestCase): values = list(aliases.values()) else: values = aliases or [] - all_domains.extend(values) - # Filter out any empty or non-string entries - domain_list = [d for d in all_domains if isinstance(d, str) and d.strip()] - counts = Counter(domain_list) + for d in values: + if isinstance(d, str) and d.strip(): + domain_to_apps[d].add(app_name) - # Find duplicates - duplicates = [domain for domain, count in counts.items() if count > 1] + # Find duplicates: domains that appear in more than one app + duplicates = {domain: list(apps) for domain, apps in domain_to_apps.items() if len(apps) > 1} if duplicates: - self.fail(f"Duplicate domain entries found: {duplicates}\n (May 'make build' solves this issue.)") + details = "\n".join(f"Domain '{domain}' is used in applications: {apps}" for domain, apps in duplicates.items()) + self.fail(f"Duplicate domain entries found:\n{details}\n(Maybe 'make build' solves this issue.)") if __name__ == "__main__": unittest.main()