Compare commits

...

8 Commits

45 changed files with 223 additions and 36 deletions

View File

@@ -67,7 +67,7 @@ _applications_nextcloud_oidc_flavor: >-
False,
'oidc_login'
if applications
| get_app_conf('web-app-nextcloud','features.ldap',False)
| get_app_conf('web-app-nextcloud','features.ldap',False, True)
else 'sociallogin'
)
}}

View File

@@ -5,7 +5,7 @@ services:
{% include 'roles/cmp-rdbms/templates/services/main.yml.j2' %}
{% endif %}
{# Load Redis #}
{% if applications | is_docker_service_enabled(application_id, 'redis') %}
{% if applications | is_docker_service_enabled(application_id, 'redis') or applications | get_app_conf(application_id, 'features.oauth2', False) %}
{% include 'roles/svc-db-redis/templates/service.yml.j2' %}
{% endif %}
{# Load OAuth2 Proxy #}

View File

@@ -1,7 +1,11 @@
{# This template needs to be included in docker-compose.yml which contain a database and additional volumes #}
volumes:
{% if not applications | get_app_conf(application_id, 'features.central_database', False) and applications | get_app_conf(application_id, 'docker.services.database.enabled', False) %}
{% if applications | is_docker_service_enabled(application_id, 'database') and not applications | get_app_conf(application_id, 'features.central_database', False) %}
database:
name: {{ database_volume }}
{% endif %}
{% if applications | is_docker_service_enabled(application_id, 'redis') or applications | get_app_conf(application_id, 'features.oauth2', False) %}
redis:
name: {{ application_id | get_entity_name }}
{% endif %}
{{ "\n" }}

View File

@@ -26,6 +26,10 @@ server {
{% include 'roles/srv-proxy-7-4-core/templates/location/proxy_basic.conf.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 ws_path is defined %}
location {{ ws_path }} {
proxy_set_header Host $host;

View File

@@ -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

View File

@@ -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

View File

@@ -0,0 +1 @@
<script>{{ javascript_code_one_liner | replace("'", "\\'") }}</script>

View File

@@ -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;
});
}
});
})();

View File

@@ -0,0 +1 @@
modifier_javascript_template_file: "{{ application_id | abs_role_path_by_application_id }}/templates/javascript.js.j2"

View File

@@ -50,6 +50,6 @@
changed_when: "'adding new entry' in ldapadd_result.stdout"
failed_when: ldapadd_result.rc not in [0, 20, 68, 65]
listen:
- "Import data LDIF files"
- "Import groups LDIF files"
- "Import all LDIF files"
loop: "{{ query('fileglob', role_path ~ '/templates/ldif/groups/*.j2') | sort }}"

View File

@@ -24,6 +24,5 @@
- .:/var/www
{% include 'roles/docker-compose/templates/volumes.yml.j2' %}
redis:
{% include 'roles/docker-compose/templates/networks.yml.j2' %}

View File

@@ -18,3 +18,7 @@ docker:
name: "baserow"
volumes:
data: "baserow_data"
domains:
canonical:
- baserow.{{ primary_domain }}

View File

@@ -14,6 +14,5 @@
{% include 'roles/docker-compose/templates/volumes.yml.j2' %}
data:
name: {{ baserow_volume }}
redis:
{% include 'roles/docker-compose/templates/networks.yml.j2' %}

View File

@@ -1,7 +1,6 @@
{% include 'roles/docker-compose/templates/base.yml.j2' %}
{% include 'roles/docker-compose/templates/volumes.yml.j2' %}
redis:
{% include 'roles/docker-compose/templates/networks.yml.j2' %}
{{ discourse_network }}:

View File

@@ -1,2 +1,6 @@
features:
universal_logout: false # Just deactivated to oppress warnings, elk is anyhow not running
domains:
canonical:
- elk.{{ primary_domain }}

View File

@@ -24,6 +24,8 @@ csp:
domains:
aliases:
- "crm.{{ primary_domain }}"
canonical:
- espocrm.{{ primary_domain }}
email:
from_name: "Customer Relationship Management ({{ primary_domain }})"
docker:

View File

@@ -63,7 +63,6 @@
{% include 'roles/docker-compose/templates/volumes.yml.j2' %}
data:
funkwhale_static_root:
redis:
music:
{% include 'roles/docker-compose/templates/networks.yml.j2' %}

View File

@@ -36,6 +36,8 @@ csp:
domains:
aliases:
- "git.{{ primary_domain }}"
canonical:
- gitea.{{ primary_domain }}
docker:
services:
database:

View File

@@ -15,3 +15,7 @@ docker:
version: "latest"
credentials:
initial_root_password: "{{ users.administrator.password }}"
domains:
canonical:
- gitlab.{{ primary_domain }}

View File

@@ -16,7 +16,6 @@
{% include 'roles/docker-container/templates/networks.yml.j2' %}
{% include 'roles/docker-compose/templates/volumes.yml.j2' %}
redis:
config:
logs:
data:

View File

@@ -1,2 +1,6 @@
features:
universal_logout: true # Same like with elk, anyhow not active atm
domains:
canonical:
- jenkins.{{ primary_domain }}

View File

@@ -11,7 +11,7 @@ features:
port-ui-desktop: true
ldap: true
central_database: false
oauth2: false
oauth2: true
universal_logout: true
csp:
flags:
@@ -25,4 +25,6 @@ csp:
domains:
aliases:
- "ldap.{{primary_domain}}"
canonical:
- lam.{{ primary_domain }}

View File

@@ -22,7 +22,8 @@ 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
canonical:
- "libretranslate.{{ primary_domain }}"
aliases: [] # Alias redirections to the first element of the canonical domains
rbac:
roles: {}

View File

@@ -100,7 +100,7 @@
- "{{docker_compose.directories.volumes}}overrides/rspamd:/overrides:ro"
depends_on:
- front
- redis
- redis
- antivirus
- resolver
dns:
@@ -181,8 +181,6 @@
name: {{ mailu_dkim }}
dovecot_mail:
name: {{ mailu_dovecot_mail }}
redis:
name: {{ mailu_redis }}
{% include 'roles/docker-compose/templates/networks.yml.j2' %}
radicale:

View File

@@ -44,7 +44,6 @@
{% include 'roles/docker-container/templates/networks.yml.j2' %}
{% include 'roles/docker-compose/templates/volumes.yml.j2' %}
redis:
data:
name: "{{ mastodon_volume }}"
{% include 'roles/docker-compose/templates/networks.yml.j2' %}

View File

@@ -13,4 +13,9 @@ server {
{% include 'roles/srv-web-7-7-inj-compose/templates/global.includes.conf.j2'%}
{% include 'roles/srv-proxy-7-4-core/templates/location/proxy_basic.conf.j2' %}
{% if applications | get_app_conf(application_id, 'features.universal_logout', False) %}
{% include 'roles/web-svc-logout/templates/logout-proxy.conf.j2' %}
{% endif %}
}

View File

@@ -15,3 +15,7 @@ docker:
name: "mybb"
volumes:
data: "mybb_data"
domains:
canonical:
- mybb.{{ primary_domain }}

View File

@@ -71,6 +71,5 @@
{% include 'roles/docker-compose/templates/volumes.yml.j2' %}
data:
name: {{ nextcloud_volume }}
redis:
{% include 'roles/docker-compose/templates/networks.yml.j2' %}

View File

@@ -20,6 +20,10 @@ server
{% include 'roles/srv-proxy-7-4-core/templates/location/proxy_basic.conf.j2' %}
{% if applications | get_app_conf(application_id, 'features.universal_logout', False) %}
{% include 'roles/web-svc-logout/templates/logout-proxy.conf.j2' %}
{% endif %}
location ^~ /.well-known {
rewrite ^/\.well-known/host-meta\.json /public.php?service=host-meta-json last;
rewrite ^/\.well-known/host-meta /public.php?service=host-meta last;

View File

@@ -42,7 +42,7 @@ nextcloud_cron_name: "{{ applications | get_app_conf(
nextcloud_talk_name: "{{ applications | get_app_conf(application_id, 'docker.services.talk.name', True) }}"
nextcloud_talk_image: "{{ applications | get_app_conf(application_id, 'docker.services.talk.image', True) }}"
nextcloud_talk_version: "{{ applications | get_app_conf(application_id, 'docker.services.talk.version', True) }}"
nextcloud_talk_enabled: "{{ applications | get_app_conf(application_id, 'docker.services.talk.enabled', True) }}"
nextcloud_talk_enabled: "{{ applications | is_docker_service_enabled(application_id, 'talk') }}"
nextcloud_talk_stun_port: "{{ ports.public.stun[application_id] }}"
# nextcloud_talk_domain: "{{ domains[application_id].talk }}"

View File

@@ -6,3 +6,7 @@ features:
css: true
port-ui-desktop: false
universal_logout: true
domains:
canonical:
- oauth2-proxy.{{ primary_domain }}

View File

@@ -21,4 +21,7 @@ allowed_groups = {{ applications | get_app_conf(oauth2_proxy_applicat
email_domains = ["*"]
{% else %}
email_domains = "{{ primary_domain }}"
{% endif %}
{% endif %}
session_store_type = "redis"
redis_connection_url = "redis://redis:6379"

View File

@@ -19,7 +19,6 @@
assets:
data:
name: {{ peertube_volume }}
redis:
config:
{% include 'roles/docker-compose/templates/networks.yml.j2' %}

View File

@@ -27,6 +27,10 @@ server {
proxy_pass http://127.0.0.1:{{ports.localhost.http[application_id]}};
}
{% if applications | get_app_conf(application_id, 'features.universal_logout', False) %}
{% include 'roles/web-svc-logout/templates/logout-proxy.conf.j2' %}
{% endif %}
location / {
try_files /dev/null @api;
}

View File

@@ -26,3 +26,7 @@ docker:
services:
database:
enabled: true
domains:
canonical:
- pgadmin.{{ primary_domain }}

View File

@@ -11,3 +11,7 @@ features:
ldap: true
oauth2: true
universal_logout: true
domains:
canonical:
- phpldapadmin.{{ primary_domain }}

View File

@@ -22,6 +22,8 @@ domains:
aliases:
- "mysql.{{ primary_domain }}"
- "mariadb.{{ primary_domain }}"
canonical:
- phpmyadmin.{{ primary_domain }}
docker:
services:
database:

View File

@@ -30,7 +30,6 @@
{% include 'roles/docker-container/templates/networks.yml.j2' %}
{% include 'roles/docker-compose/templates/volumes.yml.j2' %}
redis:
data:
name: {{ pixelfed_volume }}

View File

@@ -18,11 +18,12 @@ features:
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
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
canonical:
- "pretix.{{ primary_domain }}"
aliases: [] # Alias redirections to the first element of the canonical domains
rbac:
roles: {}

View File

@@ -18,7 +18,6 @@
# Compose Configuration
{% include 'roles/docker-compose/templates/volumes.yml.j2' %}
redis:
data:
name: "{{ snipe_it_volume }}"

View File

@@ -13,3 +13,7 @@ features:
# users:
# administrator:
# username: "{{ users.administrator.username }}"
domains:
canonical:
- syncope.{{ primary_domain }}

View File

@@ -1,3 +1,7 @@
# 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
domains:
canonical:
- xmpp.{{ primary_domain }}

View File

@@ -1,2 +1,6 @@
source_directory: "{{ playbook_dir }}/assets"
url: "{{ web_protocol }}://<< defaults_applications['web-svc-file']domains.canonical[0] >>/assets"
url: "{{ web_protocol }}://<< defaults_applications['web-svc-file']domains.canonical[0] >>/assets"
domains:
canonical:
- asset.{{ primary_domain }}

View File

@@ -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()

View File

@@ -0,0 +1,36 @@
import unittest
import yaml
import glob
import os
class TestWebRolesDomains(unittest.TestCase):
def test_canonical_domains_present_and_not_empty(self):
"""
Check all roles/web-*/config/main.yml files:
- must have domains.canonical defined
- domains.canonical must not be empty dict, empty list, or empty string
"""
role_config_paths = glob.glob("roles/web-*/config/main.yml")
self.assertTrue(role_config_paths, "No roles/web-*/config/main.yml files found.")
for path in role_config_paths:
with self.subTest(role_config=path):
with open(path, "r") as f:
data = yaml.safe_load(f)
self.assertIsInstance(data, dict, f"YAML root is not a dict in {path}")
domains = data.get("domains")
self.assertIsNotNone(domains, f"'domains' section missing in {path}")
self.assertIsInstance(domains, dict, f"'domains' must be a dict in {path}")
canonical = domains.get("canonical")
self.assertIsNotNone(canonical, f"'domains.canonical' missing in {path}")
# Check for emptiness
empty_values = [{}, [], ""]
self.assertNotIn(canonical, empty_values,
f"'domains.canonical' in {path} must not be empty dict, list, or empty string")
if __name__ == "__main__":
unittest.main()