mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2025-09-08 19:27:18 +02:00
Compare commits
9 Commits
c185c537cb
...
ff18c7cd73
Author | SHA1 | Date | |
---|---|---|---|
ff18c7cd73 | |||
a84abbdade | |||
5dc8ec2344 | |||
4b9e7dd3b7 | |||
22ff2dc1f3 | |||
16c1a5d834 | |||
b25f7f52b3 | |||
4826de621e | |||
4501c31756 |
@@ -122,15 +122,23 @@ class FilterModule(object):
|
|||||||
tokens.append('https://www.gstatic.com')
|
tokens.append('https://www.gstatic.com')
|
||||||
tokens.append('https://www.google.com')
|
tokens.append('https://www.google.com')
|
||||||
|
|
||||||
# Enable loading via ancestors
|
if directive == 'frame-ancestors':
|
||||||
if (
|
# Enable loading via ancestors
|
||||||
self.is_feature_enabled(applications, 'port-ui-desktop', application_id)
|
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"
|
||||||
domain = domains.get('web-app-port-ui')[0]
|
tokens.append(f"{sld_tld}") # yields "*.example.com"
|
||||||
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
|
# whitelist
|
||||||
tokens += self.get_csp_whitelist(applications, application_id, directive)
|
tokens += self.get_csp_whitelist(applications, application_id, directive)
|
||||||
|
|
||||||
|
@@ -1,151 +1,6 @@
|
|||||||
import os
|
import sys, os
|
||||||
import re
|
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
|
||||||
import yaml
|
from module_utils.config_utils import get_app_conf, AppConfigKeyError,ConfigEntryNotSetError
|
||||||
from ansible.errors import AnsibleFilterError
|
|
||||||
from collections.abc import Mapping
|
|
||||||
|
|
||||||
from ansible.errors import AnsibleUndefinedVariable
|
|
||||||
try:
|
|
||||||
from ansible.utils.unsafe_proxy import AnsibleUndefined
|
|
||||||
except ImportError:
|
|
||||||
class AnsibleUndefined: pass
|
|
||||||
|
|
||||||
class AppConfigKeyError(AnsibleFilterError, ValueError):
|
|
||||||
"""
|
|
||||||
Raised when a required application config key is missing (strict mode).
|
|
||||||
Compatible with Ansible error handling and Python ValueError.
|
|
||||||
"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
class ConfigEntryNotSetError(AppConfigKeyError):
|
|
||||||
"""
|
|
||||||
Raised when a config entry is defined in schema but not set in application.
|
|
||||||
"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def get_app_conf(applications, application_id, config_path, strict=True, default=None):
|
|
||||||
# Path to the schema file for this application
|
|
||||||
schema_path = os.path.join('roles', application_id, 'schema', 'main.yml')
|
|
||||||
|
|
||||||
def schema_defines(path):
|
|
||||||
if not os.path.isfile(schema_path):
|
|
||||||
return False
|
|
||||||
with open(schema_path) as f:
|
|
||||||
schema = yaml.safe_load(f) or {}
|
|
||||||
node = schema
|
|
||||||
for part in path.split('.'):
|
|
||||||
key_match = re.match(r"^([a-zA-Z0-9_-]+)", part)
|
|
||||||
if not key_match:
|
|
||||||
return False
|
|
||||||
k = key_match.group(1)
|
|
||||||
if isinstance(node, dict) and k in node:
|
|
||||||
node = node[k]
|
|
||||||
else:
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
|
|
||||||
def access(obj, key, path_trace):
|
|
||||||
# Match either 'key' or 'key[index]'
|
|
||||||
m = re.match(r"^([a-zA-Z0-9_-]+)(?:\[(\d+)\])?$", key)
|
|
||||||
if not m:
|
|
||||||
raise AppConfigKeyError(
|
|
||||||
f"Invalid key format in config_path: '{key}'\n"
|
|
||||||
f"Full path so far: {'.'.join(path_trace)}\n"
|
|
||||||
f"application_id: {application_id}\n"
|
|
||||||
f"config_path: {config_path}"
|
|
||||||
)
|
|
||||||
k, idx = m.group(1), m.group(2)
|
|
||||||
|
|
||||||
if (hasattr(obj, '__class__') and obj.__class__.__name__ == 'AnsibleUndefined') \
|
|
||||||
or isinstance(obj, AnsibleUndefinedVariable):
|
|
||||||
if not strict:
|
|
||||||
return default if default is not None else False
|
|
||||||
raise AppConfigKeyError(
|
|
||||||
f"Key '{k}' is undefined at '{'.'.join(path_trace)}'\n"
|
|
||||||
f" actual type: {type(obj).__name__}\n"
|
|
||||||
f" repr(obj): {obj!r}\n"
|
|
||||||
f" repr(applications): {applications!r}\n"
|
|
||||||
f"application_id: {application_id}\n"
|
|
||||||
f"config_path: {config_path}"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Access dict key
|
|
||||||
if isinstance(obj, Mapping):
|
|
||||||
if k not in obj:
|
|
||||||
# Non-strict mode: always return default on missing key
|
|
||||||
if not strict:
|
|
||||||
return default if default is not None else False
|
|
||||||
# Schema-defined but unset: strict raises ConfigEntryNotSetError
|
|
||||||
trace_path = '.'.join(path_trace[1:])
|
|
||||||
if schema_defines(trace_path):
|
|
||||||
raise ConfigEntryNotSetError(
|
|
||||||
f"Config entry '{trace_path}' is defined in schema at '{schema_path}' but not set in application '{application_id}'."
|
|
||||||
)
|
|
||||||
# Generic missing-key error
|
|
||||||
raise AppConfigKeyError(
|
|
||||||
f"Key '{k}' not found in dict at '{key}'\n"
|
|
||||||
f"Full path so far: {'.'.join(path_trace)}\n"
|
|
||||||
f"Current object: {repr(obj)}\n"
|
|
||||||
f"application_id: {application_id}\n"
|
|
||||||
f"config_path: {config_path}"
|
|
||||||
)
|
|
||||||
obj = obj[k]
|
|
||||||
else:
|
|
||||||
if not strict:
|
|
||||||
return default if default is not None else False
|
|
||||||
raise AppConfigKeyError(
|
|
||||||
f"Expected dict for '{k}', got {type(obj).__name__} at '{key}'\n"
|
|
||||||
f"Full path so far: {'.'.join(path_trace)}\n"
|
|
||||||
f"Current object: {repr(obj)}\n"
|
|
||||||
f"application_id: {application_id}\n"
|
|
||||||
f"config_path: {config_path}"
|
|
||||||
)
|
|
||||||
|
|
||||||
# If index was provided, access list element
|
|
||||||
if idx is not None:
|
|
||||||
if not isinstance(obj, list):
|
|
||||||
if not strict:
|
|
||||||
return default if default is not None else False
|
|
||||||
raise AppConfigKeyError(
|
|
||||||
f"Expected list for '{k}[{idx}]', got {type(obj).__name__}\n"
|
|
||||||
f"Full path so far: {'.'.join(path_trace)}\n"
|
|
||||||
f"Current object: {repr(obj)}\n"
|
|
||||||
f"application_id: {application_id}\n"
|
|
||||||
f"config_path: {config_path}"
|
|
||||||
)
|
|
||||||
i = int(idx)
|
|
||||||
if i >= len(obj):
|
|
||||||
if not strict:
|
|
||||||
return default if default is not None else False
|
|
||||||
raise AppConfigKeyError(
|
|
||||||
f"Index {i} out of range for list at '{k}'\n"
|
|
||||||
f"Full path so far: {'.'.join(path_trace)}\n"
|
|
||||||
f"Current object: {repr(obj)}\n"
|
|
||||||
f"application_id: {application_id}\n"
|
|
||||||
f"config_path: {config_path}"
|
|
||||||
)
|
|
||||||
obj = obj[i]
|
|
||||||
return obj
|
|
||||||
|
|
||||||
# Begin traversal
|
|
||||||
path_trace = [f"applications[{repr(application_id)}]"]
|
|
||||||
try:
|
|
||||||
obj = applications[application_id]
|
|
||||||
except KeyError:
|
|
||||||
raise AppConfigKeyError(
|
|
||||||
f"Application ID '{application_id}' not found in applications dict.\n"
|
|
||||||
f"path_trace: {path_trace}\n"
|
|
||||||
f"applications keys: {list(applications.keys())}\n"
|
|
||||||
f"config_path: {config_path}"
|
|
||||||
)
|
|
||||||
|
|
||||||
for part in config_path.split('.'):
|
|
||||||
path_trace.append(part)
|
|
||||||
obj = access(obj, part, path_trace)
|
|
||||||
if obj is False and not strict:
|
|
||||||
return default if default is not None else False
|
|
||||||
return obj
|
|
||||||
|
|
||||||
class FilterModule(object):
|
class FilterModule(object):
|
||||||
''' CyMaIS application config extraction filters '''
|
''' CyMaIS application config extraction filters '''
|
||||||
|
@@ -17,5 +17,4 @@ nginx:
|
|||||||
cache:
|
cache:
|
||||||
general: "/tmp/cache_nginx_general/" # Directory which nginx uses to cache general data
|
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
|
image: "/tmp/cache_nginx_image/" # Directory which nginx uses to cache images
|
||||||
user: "http" # Default nginx user in ArchLinux
|
user: "http" # Default nginx user in ArchLinux
|
||||||
iframe: true # Allows applications to be loaded in iframe
|
|
@@ -69,6 +69,7 @@ ports:
|
|||||||
web-app-libretranslate: 8045
|
web-app-libretranslate: 8045
|
||||||
web-app-pretix: 8046
|
web-app-pretix: 8046
|
||||||
web-app-mig: 8047
|
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
|
web-app-bigbluebutton: 48087 # This port is predefined by bbb. @todo Try to change this to a 8XXX port
|
||||||
public:
|
public:
|
||||||
# The following ports should be changed to 22 on the subdomain via stream mapping
|
# The following ports should be changed to 22 on the subdomain via stream mapping
|
||||||
|
@@ -94,6 +94,8 @@ defaults_networks:
|
|||||||
subnet: 192.168.103.144/28
|
subnet: 192.168.103.144/28
|
||||||
web-app-mig:
|
web-app-mig:
|
||||||
subnet: 192.168.103.160/28
|
subnet: 192.168.103.160/28
|
||||||
|
web-svc-logout:
|
||||||
|
subnet: 192.168.103.176/28
|
||||||
|
|
||||||
# /24 Networks / 254 Usable Clients
|
# /24 Networks / 254 Usable Clients
|
||||||
web-app-bigbluebutton:
|
web-app-bigbluebutton:
|
||||||
|
149
module_utils/config_utils.py
Normal file
149
module_utils/config_utils.py
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
import os
|
||||||
|
import re
|
||||||
|
import yaml
|
||||||
|
from ansible.errors import AnsibleFilterError
|
||||||
|
from collections.abc import Mapping
|
||||||
|
|
||||||
|
from ansible.errors import AnsibleUndefinedVariable
|
||||||
|
try:
|
||||||
|
from ansible.utils.unsafe_proxy import AnsibleUndefined
|
||||||
|
except ImportError:
|
||||||
|
class AnsibleUndefined: pass
|
||||||
|
|
||||||
|
class AppConfigKeyError(AnsibleFilterError, ValueError):
|
||||||
|
"""
|
||||||
|
Raised when a required application config key is missing (strict mode).
|
||||||
|
Compatible with Ansible error handling and Python ValueError.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
class ConfigEntryNotSetError(AppConfigKeyError):
|
||||||
|
"""
|
||||||
|
Raised when a config entry is defined in schema but not set in application.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def get_app_conf(applications, application_id, config_path, strict=True, default=None):
|
||||||
|
# Path to the schema file for this application
|
||||||
|
schema_path = os.path.join('roles', application_id, 'schema', 'main.yml')
|
||||||
|
|
||||||
|
def schema_defines(path):
|
||||||
|
if not os.path.isfile(schema_path):
|
||||||
|
return False
|
||||||
|
with open(schema_path) as f:
|
||||||
|
schema = yaml.safe_load(f) or {}
|
||||||
|
node = schema
|
||||||
|
for part in path.split('.'):
|
||||||
|
key_match = re.match(r"^([a-zA-Z0-9_-]+)", part)
|
||||||
|
if not key_match:
|
||||||
|
return False
|
||||||
|
k = key_match.group(1)
|
||||||
|
if isinstance(node, dict) and k in node:
|
||||||
|
node = node[k]
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
def access(obj, key, path_trace):
|
||||||
|
# Match either 'key' or 'key[index]'
|
||||||
|
m = re.match(r"^([a-zA-Z0-9_-]+)(?:\[(\d+)\])?$", key)
|
||||||
|
if not m:
|
||||||
|
raise AppConfigKeyError(
|
||||||
|
f"Invalid key format in config_path: '{key}'\n"
|
||||||
|
f"Full path so far: {'.'.join(path_trace)}\n"
|
||||||
|
f"application_id: {application_id}\n"
|
||||||
|
f"config_path: {config_path}"
|
||||||
|
)
|
||||||
|
k, idx = m.group(1), m.group(2)
|
||||||
|
|
||||||
|
if (hasattr(obj, '__class__') and obj.__class__.__name__ == 'AnsibleUndefined') \
|
||||||
|
or isinstance(obj, AnsibleUndefinedVariable):
|
||||||
|
if not strict:
|
||||||
|
return default if default is not None else False
|
||||||
|
raise AppConfigKeyError(
|
||||||
|
f"Key '{k}' is undefined at '{'.'.join(path_trace)}'\n"
|
||||||
|
f" actual type: {type(obj).__name__}\n"
|
||||||
|
f" repr(obj): {obj!r}\n"
|
||||||
|
f" repr(applications): {applications!r}\n"
|
||||||
|
f"application_id: {application_id}\n"
|
||||||
|
f"config_path: {config_path}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Access dict key
|
||||||
|
if isinstance(obj, Mapping):
|
||||||
|
if k not in obj:
|
||||||
|
# Non-strict mode: always return default on missing key
|
||||||
|
if not strict:
|
||||||
|
return default if default is not None else False
|
||||||
|
# Schema-defined but unset: strict raises ConfigEntryNotSetError
|
||||||
|
trace_path = '.'.join(path_trace[1:])
|
||||||
|
if schema_defines(trace_path):
|
||||||
|
raise ConfigEntryNotSetError(
|
||||||
|
f"Config entry '{trace_path}' is defined in schema at '{schema_path}' but not set in application '{application_id}'."
|
||||||
|
)
|
||||||
|
# Generic missing-key error
|
||||||
|
raise AppConfigKeyError(
|
||||||
|
f"Key '{k}' not found in dict at '{key}'\n"
|
||||||
|
f"Full path so far: {'.'.join(path_trace)}\n"
|
||||||
|
f"Current object: {repr(obj)}\n"
|
||||||
|
f"application_id: {application_id}\n"
|
||||||
|
f"config_path: {config_path}"
|
||||||
|
)
|
||||||
|
obj = obj[k]
|
||||||
|
else:
|
||||||
|
if not strict:
|
||||||
|
return default if default is not None else False
|
||||||
|
raise AppConfigKeyError(
|
||||||
|
f"Expected dict for '{k}', got {type(obj).__name__} at '{key}'\n"
|
||||||
|
f"Full path so far: {'.'.join(path_trace)}\n"
|
||||||
|
f"Current object: {repr(obj)}\n"
|
||||||
|
f"application_id: {application_id}\n"
|
||||||
|
f"config_path: {config_path}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# If index was provided, access list element
|
||||||
|
if idx is not None:
|
||||||
|
if not isinstance(obj, list):
|
||||||
|
if not strict:
|
||||||
|
return default if default is not None else False
|
||||||
|
raise AppConfigKeyError(
|
||||||
|
f"Expected list for '{k}[{idx}]', got {type(obj).__name__}\n"
|
||||||
|
f"Full path so far: {'.'.join(path_trace)}\n"
|
||||||
|
f"Current object: {repr(obj)}\n"
|
||||||
|
f"application_id: {application_id}\n"
|
||||||
|
f"config_path: {config_path}"
|
||||||
|
)
|
||||||
|
i = int(idx)
|
||||||
|
if i >= len(obj):
|
||||||
|
if not strict:
|
||||||
|
return default if default is not None else False
|
||||||
|
raise AppConfigKeyError(
|
||||||
|
f"Index {i} out of range for list at '{k}'\n"
|
||||||
|
f"Full path so far: {'.'.join(path_trace)}\n"
|
||||||
|
f"Current object: {repr(obj)}\n"
|
||||||
|
f"application_id: {application_id}\n"
|
||||||
|
f"config_path: {config_path}"
|
||||||
|
)
|
||||||
|
obj = obj[i]
|
||||||
|
return obj
|
||||||
|
|
||||||
|
# Begin traversal
|
||||||
|
path_trace = [f"applications[{repr(application_id)}]"]
|
||||||
|
try:
|
||||||
|
obj = applications[application_id]
|
||||||
|
except KeyError:
|
||||||
|
raise AppConfigKeyError(
|
||||||
|
f"Application ID '{application_id}' not found in applications dict.\n"
|
||||||
|
f"path_trace: {path_trace}\n"
|
||||||
|
f"applications keys: {list(applications.keys())}\n"
|
||||||
|
f"config_path: {config_path}"
|
||||||
|
)
|
||||||
|
|
||||||
|
for part in config_path.split('.'):
|
||||||
|
path_trace.append(part)
|
||||||
|
obj = access(obj, part, path_trace)
|
||||||
|
if obj is False and not strict:
|
||||||
|
return default if default is not None else False
|
||||||
|
return obj
|
||||||
|
|
@@ -24,7 +24,7 @@
|
|||||||
- name: docker compose up
|
- name: docker compose up
|
||||||
shell: |
|
shell: |
|
||||||
if [ -f "{{ docker_compose.files.env }}" ]; then
|
if [ -f "{{ docker_compose.files.env }}" ]; then
|
||||||
docker compose -p {{ application_id | get_entity_name }} --env-file "{{ docker_compose.files.env }} up -d --force-recreate --remove-orphans"
|
docker compose -p {{ application_id | get_entity_name }} --env-file "{{ docker_compose.files.env }}" up -d --force-recreate --remove-orphans
|
||||||
else
|
else
|
||||||
docker compose -p {{ application_id | get_entity_name }} up -d --force-recreate --remove-orphans
|
docker compose -p {{ application_id | get_entity_name }} up -d --force-recreate --remove-orphans
|
||||||
fi
|
fi
|
||||||
|
@@ -15,6 +15,9 @@ server
|
|||||||
|
|
||||||
{% include 'roles/srv-web-7-7-letsencrypt/templates/ssl_header.j2' %}
|
{% 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) %}
|
{% if applications | get_app_conf(application_id, 'features.oauth2', False) %}
|
||||||
{% set acl = applications | get_app_conf(application_id, 'oauth2_proxy.acl', False, {}) %}
|
{% set acl = applications | get_app_conf(application_id, 'oauth2_proxy.acl', False, {}) %}
|
||||||
|
|
||||||
|
@@ -68,5 +68,5 @@ docker exec -i ldap \
|
|||||||
-D "$LDAP_ADMIN_DN" \
|
-D "$LDAP_ADMIN_DN" \
|
||||||
-w "$LDAP_ADMIN_PASSWORD" \
|
-w "$LDAP_ADMIN_PASSWORD" \
|
||||||
-c \
|
-c \
|
||||||
-f "/tmp/ldif/data/01_rbac.ldif"
|
-f "/tmp/ldif/groups/01_rbac.ldif"
|
||||||
```
|
```
|
@@ -45,11 +45,11 @@
|
|||||||
|
|
||||||
- name: "Import users, groups, etc. to LDAP"
|
- name: "Import users, groups, etc. to LDAP"
|
||||||
shell: >
|
shell: >
|
||||||
docker exec -i {{ openldap_name }} ldapadd -x -D "{{ldap.dn.administrator.data}}" -w "{{ldap.bind_credential}}" -c -f "{{openldap_ldif_docker_path}}data/{{ item | basename | regex_replace('\.j2$', '') }}"
|
docker exec -i {{ openldap_name }} ldapadd -x -D "{{ldap.dn.administrator.data}}" -w "{{ldap.bind_credential}}" -c -f "{{openldap_ldif_docker_path}}groups/{{ item | basename | regex_replace('\.j2$', '') }}"
|
||||||
register: ldapadd_result
|
register: ldapadd_result
|
||||||
changed_when: "'adding new entry' in ldapadd_result.stdout"
|
changed_when: "'adding new entry' in ldapadd_result.stdout"
|
||||||
failed_when: ldapadd_result.rc not in [0, 20, 68, 65]
|
failed_when: ldapadd_result.rc not in [0, 20, 68, 65]
|
||||||
listen:
|
listen:
|
||||||
- "Import data LDIF files"
|
- "Import data LDIF files"
|
||||||
- "Import all LDIF files"
|
- "Import all LDIF files"
|
||||||
loop: "{{ query('fileglob', role_path ~ '/templates/ldif/data/*.j2') | sort }}"
|
loop: "{{ query('fileglob', role_path ~ '/templates/ldif/groups/*.j2') | sort }}"
|
@@ -12,7 +12,7 @@ openldap_ldif_host_path: "{{docker_compose.directories.volumes}}ldif/"
|
|||||||
openldap_ldif_docker_path: "/tmp/ldif/"
|
openldap_ldif_docker_path: "/tmp/ldif/"
|
||||||
openldap_ldif_types:
|
openldap_ldif_types:
|
||||||
- configuration
|
- configuration
|
||||||
- data
|
- groups
|
||||||
- schema # Don't know if this is still needed, it's now setup via tasks
|
- schema # Don't know if this is still needed, it's now setup via tasks
|
||||||
|
|
||||||
openldap_name: "{{ applications | get_app_conf(application_id, 'docker.services.openldap.name', True) }}"
|
openldap_name: "{{ applications | get_app_conf(application_id, 'docker.services.openldap.name', True) }}"
|
||||||
|
2
roles/svc-opt-ssd-hdd/Todo.md
Normal file
2
roles/svc-opt-ssd-hdd/Todo.md
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
# Todos
|
||||||
|
- Figure out why configuration in repo wasn't possible
|
3
roles/svc-opt-ssd-hdd/config/main.yml
Normal file
3
roles/svc-opt-ssd-hdd/config/main.yml
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
volumes:
|
||||||
|
mass_storage: /mnt/hdd/
|
||||||
|
rapid_storage: /mnt/ssd/
|
@@ -1,5 +1,5 @@
|
|||||||
application_id: svc-opt-ssd-hdd
|
application_id: svc-opt-ssd-hdd
|
||||||
storage_optimizer_directory: '{{ path_administrator_scripts }}{{ application_id }}/'
|
storage_optimizer_directory: '{{ path_administrator_scripts }}{{ application_id }}/'
|
||||||
storage_optimizer_script: '{{ storage_optimizer_directory }}{{ application_id }}.py'
|
storage_optimizer_script: '{{ storage_optimizer_directory }}{{ application_id }}.py'
|
||||||
path_rapid_storage: "{{ applications | get_app_conf(application_id, 'path_rapid_storage', False) }}"
|
path_rapid_storage: "{{ applications | get_app_conf(application_id, 'volumes.rapid_storage') }}"
|
||||||
path_mass_storage: "{{ applications | get_app_conf(application_id, 'path_mass_storage', False) }}"
|
path_mass_storage: "{{ applications | get_app_conf(application_id, 'volumes.mass_storage') }}"
|
||||||
|
@@ -13,7 +13,7 @@
|
|||||||
|
|
||||||
- name: Set fact for backup_docker_to_local_cleanup_script
|
- name: Set fact for backup_docker_to_local_cleanup_script
|
||||||
set_fact:
|
set_fact:
|
||||||
backup_docker_to_local_cleanup_script: "{{ pkgmgr_output.stdout.rstrip('/') ~ '/sys-cln-all.sh' }}"
|
backup_docker_to_local_cleanup_script: "{{ pkgmgr_output.stdout.rstrip('/') ~ '/cleanup-all.sh' }}"
|
||||||
changed_when: false
|
changed_when: false
|
||||||
when: run_once_cln_failed_docker_backups is not defined
|
when: run_once_cln_failed_docker_backups is not defined
|
||||||
|
|
||||||
|
@@ -7,6 +7,7 @@ features:
|
|||||||
css: true
|
css: true
|
||||||
port-ui-desktop: true
|
port-ui-desktop: true
|
||||||
central_database: true
|
central_database: true
|
||||||
|
universal_logout: true
|
||||||
domains:
|
domains:
|
||||||
canonical:
|
canonical:
|
||||||
- "accounting.{{ primary_domain }}"
|
- "accounting.{{ primary_domain }}"
|
||||||
|
@@ -4,8 +4,9 @@ image:
|
|||||||
features:
|
features:
|
||||||
matomo: true
|
matomo: true
|
||||||
css: true
|
css: true
|
||||||
port-ui-desktop: true
|
port-ui-desktop: true
|
||||||
central_database: true
|
central_database: true
|
||||||
|
universal_logout: true
|
||||||
docker:
|
docker:
|
||||||
services:
|
services:
|
||||||
redis:
|
redis:
|
||||||
@@ -14,4 +15,4 @@ docker:
|
|||||||
enabled: true
|
enabled: true
|
||||||
domains:
|
domains:
|
||||||
canonical:
|
canonical:
|
||||||
- "tickets.{{ primary_domain }}"
|
- "tickets.{{ primary_domain }}"
|
||||||
|
@@ -3,6 +3,7 @@ features:
|
|||||||
css: true
|
css: true
|
||||||
port-ui-desktop: true
|
port-ui-desktop: true
|
||||||
central_database: true
|
central_database: true
|
||||||
|
universal_logout: true
|
||||||
docker:
|
docker:
|
||||||
services:
|
services:
|
||||||
redis:
|
redis:
|
||||||
|
@@ -7,11 +7,12 @@ api_suffix: "/bigbluebutton/"
|
|||||||
features:
|
features:
|
||||||
matomo: true
|
matomo: true
|
||||||
css: 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
|
# @todo fix this
|
||||||
ldap: false
|
ldap: false
|
||||||
oidc: true
|
oidc: true
|
||||||
central_database: false
|
central_database: false
|
||||||
|
universal_logout: true
|
||||||
domains:
|
domains:
|
||||||
canonical:
|
canonical:
|
||||||
- "meet.{{ primary_domain }}"
|
- "meet.{{ primary_domain }}"
|
||||||
@@ -21,4 +22,4 @@ csp:
|
|||||||
unsafe-inline: true
|
unsafe-inline: true
|
||||||
style-src:
|
style-src:
|
||||||
unsafe-inline: true
|
unsafe-inline: true
|
||||||
credentials: {}
|
credentials: {}
|
||||||
|
@@ -22,6 +22,7 @@
|
|||||||
docker_compose_file_final: "{{ docker_compose.directories.instance }}docker-compose.yml"
|
docker_compose_file_final: "{{ docker_compose.directories.instance }}docker-compose.yml"
|
||||||
|
|
||||||
- name: deploy .env
|
- name: deploy .env
|
||||||
|
# This seems redundant @todo Checkout if this is true and if so, delete it
|
||||||
template:
|
template:
|
||||||
src: env.j2
|
src: env.j2
|
||||||
dest: "{{ bbb_env_file_origine }}"
|
dest: "{{ bbb_env_file_origine }}"
|
||||||
|
@@ -5,8 +5,9 @@ pds:
|
|||||||
features:
|
features:
|
||||||
matomo: true
|
matomo: true
|
||||||
css: true
|
css: true
|
||||||
port-ui-desktop: true
|
port-ui-desktop: true
|
||||||
central_database: true
|
central_database: true
|
||||||
|
universal_logout: true
|
||||||
domains:
|
domains:
|
||||||
canonical:
|
canonical:
|
||||||
web: "bskyweb.{{ primary_domain }}"
|
web: "bskyweb.{{ primary_domain }}"
|
||||||
@@ -14,4 +15,4 @@ domains:
|
|||||||
docker:
|
docker:
|
||||||
services:
|
services:
|
||||||
database:
|
database:
|
||||||
enabled: true
|
enabled: true
|
||||||
|
@@ -6,4 +6,6 @@ docker:
|
|||||||
redis:
|
redis:
|
||||||
enabled: true
|
enabled: true
|
||||||
database:
|
database:
|
||||||
enabled: false # May this is wrong. Just set during refactoring
|
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 ;)
|
||||||
|
@@ -6,6 +6,7 @@ features:
|
|||||||
oidc: true
|
oidc: true
|
||||||
central_database: true
|
central_database: true
|
||||||
ldap: false # @todo implement and activate
|
ldap: false # @todo implement and activate
|
||||||
|
universal_logout: true
|
||||||
csp:
|
csp:
|
||||||
flags:
|
flags:
|
||||||
style-src:
|
style-src:
|
||||||
@@ -44,4 +45,4 @@ plugins:
|
|||||||
discourse-solved:
|
discourse-solved:
|
||||||
enabled: true
|
enabled: true
|
||||||
discourse-voting:
|
discourse-voting:
|
||||||
enabled: true
|
enabled: true
|
||||||
|
@@ -1 +1,2 @@
|
|||||||
|
features:
|
||||||
|
universal_logout: false # Just deactivated to oppress warnings, elk is anyhow not running
|
||||||
|
@@ -1,10 +1,11 @@
|
|||||||
features:
|
features:
|
||||||
matomo: true
|
matomo: true
|
||||||
css: false
|
css: false
|
||||||
port-ui-desktop: true
|
port-ui-desktop: false # @todo Solve https://chatgpt.com/c/687a50b4-8d78-800f-a202-1631aa05fd4f before setting it to true
|
||||||
ldap: false
|
ldap: false
|
||||||
oidc: true
|
oidc: true
|
||||||
central_database: true
|
central_database: true
|
||||||
|
universal_logout: true
|
||||||
csp:
|
csp:
|
||||||
flags:
|
flags:
|
||||||
script-src-elem:
|
script-src-elem:
|
||||||
@@ -34,4 +35,4 @@ docker:
|
|||||||
version: "latest"
|
version: "latest"
|
||||||
name: "espocrm"
|
name: "espocrm"
|
||||||
volumes:
|
volumes:
|
||||||
data: espocrm_data
|
data: espocrm_data
|
||||||
|
@@ -3,11 +3,12 @@ images:
|
|||||||
features:
|
features:
|
||||||
matomo: true
|
matomo: true
|
||||||
css: false # Temporary deactivated
|
css: false # Temporary deactivated
|
||||||
port-ui-desktop: true
|
port-ui-desktop: true
|
||||||
oidc: false # Implementation doesn't work yet
|
oidc: false # Implementation doesn't work yet
|
||||||
central_database: true
|
central_database: true
|
||||||
ldap: true
|
ldap: true
|
||||||
oauth2: false # No special login side which could be protected, use 2FA of Friendica instead
|
oauth2: false # No special login side which could be protected, use 2FA of Friendica instead
|
||||||
|
universal_logout: true
|
||||||
domains:
|
domains:
|
||||||
canonical:
|
canonical:
|
||||||
- "social.{{ primary_domain }}"
|
- "social.{{ primary_domain }}"
|
||||||
@@ -29,4 +30,4 @@ addons:
|
|||||||
docker:
|
docker:
|
||||||
services:
|
services:
|
||||||
database:
|
database:
|
||||||
enabled: true
|
enabled: true
|
||||||
|
@@ -15,10 +15,11 @@ docker:
|
|||||||
features:
|
features:
|
||||||
matomo: true
|
matomo: true
|
||||||
css: false
|
css: false
|
||||||
port-ui-desktop: true
|
port-ui-desktop: true
|
||||||
ldap: true
|
ldap: true
|
||||||
central_database: true
|
central_database: true
|
||||||
oauth2: false # Doesn't make sense to activate it atm, because login is possible on homepage
|
oauth2: false # Doesn't make sense to activate it atm, because login is possible on homepage
|
||||||
|
universal_logout: true
|
||||||
domains:
|
domains:
|
||||||
canonical:
|
canonical:
|
||||||
- "audio.{{ primary_domain }}"
|
- "audio.{{ primary_domain }}"
|
||||||
@@ -37,4 +38,4 @@ oauth2_proxy:
|
|||||||
port: "80"
|
port: "80"
|
||||||
acl:
|
acl:
|
||||||
blacklist:
|
blacklist:
|
||||||
- "/login"
|
- "/login"
|
||||||
|
@@ -12,6 +12,7 @@ features:
|
|||||||
ldap: true
|
ldap: true
|
||||||
oauth2: true
|
oauth2: true
|
||||||
oidc: false # Deactivated because users aren't auto-created.
|
oidc: false # Deactivated because users aren't auto-created.
|
||||||
|
universal_logout: true
|
||||||
oauth2_proxy:
|
oauth2_proxy:
|
||||||
application: "application"
|
application: "application"
|
||||||
port: "<< defaults_applications[web-app-gitea].docker.services.gitea.port >>"
|
port: "<< defaults_applications[web-app-gitea].docker.services.gitea.port >>"
|
||||||
@@ -47,4 +48,4 @@ docker:
|
|||||||
port: 3000
|
port: 3000
|
||||||
name: "gitea"
|
name: "gitea"
|
||||||
volumes:
|
volumes:
|
||||||
data: "gitea_data"
|
data: "gitea_data"
|
||||||
|
@@ -1,8 +1,9 @@
|
|||||||
features:
|
features:
|
||||||
matomo: true
|
matomo: true
|
||||||
css: true
|
css: true
|
||||||
port-ui-desktop: true
|
port-ui-desktop: true
|
||||||
central_database: true
|
central_database: true
|
||||||
|
universal_logout: true
|
||||||
docker:
|
docker:
|
||||||
services:
|
services:
|
||||||
redis:
|
redis:
|
||||||
@@ -13,4 +14,4 @@ docker:
|
|||||||
image: "gitlab/gitlab-ee"
|
image: "gitlab/gitlab-ee"
|
||||||
version: "latest"
|
version: "latest"
|
||||||
credentials:
|
credentials:
|
||||||
initial_root_password: "{{ users.administrator.password }}"
|
initial_root_password: "{{ users.administrator.password }}"
|
||||||
|
@@ -0,0 +1,2 @@
|
|||||||
|
features:
|
||||||
|
universal_logout: true # Same like with elk, anyhow not active atm
|
||||||
|
@@ -3,12 +3,13 @@ images:
|
|||||||
features:
|
features:
|
||||||
matomo: true
|
matomo: true
|
||||||
css: true
|
css: true
|
||||||
port-ui-desktop: true
|
port-ui-desktop: true
|
||||||
central_database: true
|
central_database: true
|
||||||
|
universal_logout: true
|
||||||
domains:
|
domains:
|
||||||
canonical:
|
canonical:
|
||||||
- "cms.{{ primary_domain }}"
|
- "cms.{{ primary_domain }}"
|
||||||
docker:
|
docker:
|
||||||
services:
|
services:
|
||||||
database:
|
database:
|
||||||
enabled: true
|
enabled: true
|
||||||
|
@@ -6,6 +6,7 @@ features:
|
|||||||
ldap: true
|
ldap: true
|
||||||
central_database: true
|
central_database: true
|
||||||
recaptcha: true
|
recaptcha: true
|
||||||
|
universal_logout: true
|
||||||
csp:
|
csp:
|
||||||
flags:
|
flags:
|
||||||
script-src-elem:
|
script-src-elem:
|
||||||
@@ -14,6 +15,9 @@ csp:
|
|||||||
unsafe-inline: true
|
unsafe-inline: true
|
||||||
style-src:
|
style-src:
|
||||||
unsafe-inline: true
|
unsafe-inline: true
|
||||||
|
whitelist:
|
||||||
|
frame-src:
|
||||||
|
- "*" # For frontend channel logout it's necessary that iframes can be loaded
|
||||||
domains:
|
domains:
|
||||||
canonical:
|
canonical:
|
||||||
- "auth.{{ primary_domain }}"
|
- "auth.{{ primary_domain }}"
|
||||||
|
@@ -20,4 +20,6 @@ galaxy_info:
|
|||||||
logo:
|
logo:
|
||||||
class: "fa-solid fa-lock"
|
class: "fa-solid fa-lock"
|
||||||
run_after:
|
run_after:
|
||||||
- web-app-matomo
|
- web-app-matomo
|
||||||
|
dependencies:
|
||||||
|
- web-svc-logout
|
@@ -8,10 +8,11 @@ oauth2_proxy:
|
|||||||
features:
|
features:
|
||||||
matomo: true
|
matomo: true
|
||||||
css: true
|
css: true
|
||||||
port-ui-desktop: true
|
port-ui-desktop: true
|
||||||
ldap: true
|
ldap: true
|
||||||
central_database: false
|
central_database: false
|
||||||
oauth2: false
|
oauth2: false
|
||||||
|
universal_logout: true
|
||||||
csp:
|
csp:
|
||||||
flags:
|
flags:
|
||||||
style-src:
|
style-src:
|
||||||
|
@@ -5,9 +5,9 @@ docker:
|
|||||||
versions: {} # @todo Move under services
|
versions: {} # @todo Move under services
|
||||||
services:
|
services:
|
||||||
redis:
|
redis:
|
||||||
enabled: false # Enable Redis
|
enabled: false # Enable Redis
|
||||||
database:
|
database:
|
||||||
enabled: false # Enable the database
|
enabled: false # Enable the database
|
||||||
features:
|
features:
|
||||||
matomo: true # Enable Matomo Tracking
|
matomo: true # Enable Matomo Tracking
|
||||||
css: true # Enable Global CSS Styling
|
css: true # Enable Global CSS Styling
|
||||||
@@ -16,10 +16,11 @@ features:
|
|||||||
central_database: false # Enable Central Database Network
|
central_database: false # Enable Central Database Network
|
||||||
recaptcha: false # Enable ReCaptcha
|
recaptcha: false # Enable ReCaptcha
|
||||||
oauth2: false # Enable the OAuth2-Proy
|
oauth2: false # Enable the OAuth2-Proy
|
||||||
javascript: false # Enables the custom JS in the javascript.js.j2 file
|
javascript: false # Enables the custom JS in the javascript.js.j2 file
|
||||||
csp:
|
universal_logout: false # With this app I assume that it's a service, so should be renamed and logging is unneccessary
|
||||||
whitelist: {} # URL's which should be whitelisted
|
csp:
|
||||||
flags: {} # Flags which should be set
|
whitelist: {} # URL's which should be whitelisted
|
||||||
|
flags: {} # Flags which should be set
|
||||||
domains:
|
domains:
|
||||||
canonical: {} # Urls under which the domain should be directly accessible
|
canonical: {} # Urls under which the domain should be directly accessible
|
||||||
aliases: [] # Alias redirections to the first element of the canonical domains
|
aliases: [] # Alias redirections to the first element of the canonical domains
|
||||||
|
@@ -5,6 +5,7 @@ features:
|
|||||||
port-ui-desktop: true
|
port-ui-desktop: true
|
||||||
central_database: true
|
central_database: true
|
||||||
oidc: true
|
oidc: true
|
||||||
|
universal_logout: true
|
||||||
domains:
|
domains:
|
||||||
canonical:
|
canonical:
|
||||||
- "newsletter.{{ primary_domain }}"
|
- "newsletter.{{ primary_domain }}"
|
||||||
@@ -18,4 +19,4 @@ docker:
|
|||||||
backup:
|
backup:
|
||||||
no_stop_required: true
|
no_stop_required: true
|
||||||
name: listmonk
|
name: listmonk
|
||||||
port: 9000
|
port: 9000
|
||||||
|
@@ -5,9 +5,10 @@ domain: "{{primary_domain}}" # The main domain fr
|
|||||||
features:
|
features:
|
||||||
matomo: true
|
matomo: true
|
||||||
css: false
|
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
|
oidc: true
|
||||||
central_database: false # Deactivate central database for mailu, I don't know why the database deactivation is necessary
|
central_database: false # Deactivate central database for mailu, I don't know why the database deactivation is necessary
|
||||||
|
universal_logout: true
|
||||||
domains:
|
domains:
|
||||||
canonical:
|
canonical:
|
||||||
- "mail.{{ primary_domain }}"
|
- "mail.{{ primary_domain }}"
|
||||||
@@ -32,4 +33,4 @@ docker:
|
|||||||
enabled: true
|
enabled: true
|
||||||
mailu:
|
mailu:
|
||||||
version: "2024.06" # Docker Image Version
|
version: "2024.06" # Docker Image Version
|
||||||
name: mailu
|
name: mailu
|
||||||
|
@@ -6,6 +6,7 @@ features:
|
|||||||
port-ui-desktop: true
|
port-ui-desktop: true
|
||||||
oidc: true
|
oidc: true
|
||||||
central_database: true
|
central_database: true
|
||||||
|
universal_logout: true
|
||||||
domains:
|
domains:
|
||||||
canonical:
|
canonical:
|
||||||
- "microblog.{{ primary_domain }}"
|
- "microblog.{{ primary_domain }}"
|
||||||
@@ -30,4 +31,4 @@ docker:
|
|||||||
version: latest
|
version: latest
|
||||||
name: "mastodon-streaming"
|
name: "mastodon-streaming"
|
||||||
volumes:
|
volumes:
|
||||||
data: "mastodon_data"
|
data: "mastodon_data"
|
||||||
|
@@ -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
|
port-ui-desktop: false # Didn't work in frame didn't have high priority @todo figure out pcause and solve it
|
||||||
central_database: true
|
central_database: true
|
||||||
oauth2: false
|
oauth2: false
|
||||||
|
universal_logout: true
|
||||||
csp:
|
csp:
|
||||||
whitelist:
|
whitelist:
|
||||||
script-src-elem:
|
script-src-elem:
|
||||||
@@ -43,4 +44,4 @@ docker:
|
|||||||
redis:
|
redis:
|
||||||
enabled: false
|
enabled: false
|
||||||
volumes:
|
volumes:
|
||||||
data: matomo_data
|
data: matomo_data
|
||||||
|
@@ -23,6 +23,7 @@ features:
|
|||||||
port-ui-desktop: true
|
port-ui-desktop: true
|
||||||
oidc: true # Deactivated OIDC due to this issue https://github.com/matrix-org/synapse/issues/10492
|
oidc: true # Deactivated OIDC due to this issue https://github.com/matrix-org/synapse/issues/10492
|
||||||
central_database: true
|
central_database: true
|
||||||
|
universal_logout: true
|
||||||
csp:
|
csp:
|
||||||
flags:
|
flags:
|
||||||
script-src:
|
script-src:
|
||||||
@@ -52,4 +53,4 @@ plugins:
|
|||||||
domains:
|
domains:
|
||||||
canonical:
|
canonical:
|
||||||
synapse: "matrix.{{ primary_domain }}"
|
synapse: "matrix.{{ primary_domain }}"
|
||||||
element: "element.{{ primary_domain }}"
|
element: "element.{{ primary_domain }}"
|
||||||
|
@@ -10,4 +10,6 @@ docker:
|
|||||||
no_stop_required: true
|
no_stop_required: true
|
||||||
name: mediawiki
|
name: mediawiki
|
||||||
volumes:
|
volumes:
|
||||||
data: mediawiki_data
|
data: mediawiki_data
|
||||||
|
features:
|
||||||
|
universal_logout: true
|
||||||
|
@@ -8,6 +8,7 @@ features:
|
|||||||
matomo: true # activate tracking
|
matomo: true # activate tracking
|
||||||
css: true # use custom cymais stile
|
css: true # use custom cymais stile
|
||||||
port-ui-desktop: true # Enable in port-ui
|
port-ui-desktop: true # Enable in port-ui
|
||||||
|
universal_logout: false
|
||||||
csp:
|
csp:
|
||||||
whitelist:
|
whitelist:
|
||||||
script-src-elem:
|
script-src-elem:
|
||||||
@@ -24,8 +25,8 @@ csp:
|
|||||||
- https://cdn.jsdelivr.net
|
- https://cdn.jsdelivr.net
|
||||||
connect-src:
|
connect-src:
|
||||||
- https://ka-f.fontawesome.com
|
- https://ka-f.fontawesome.com
|
||||||
#frame-src:
|
frame-ancestors:
|
||||||
# - "{{ web_protocol }}://*.{{primary_domain}}"
|
- "*" # No damage if it's used somewhere on other websites, it anyhow looks like art
|
||||||
flags:
|
flags:
|
||||||
style-src:
|
style-src:
|
||||||
unsafe-inline: true
|
unsafe-inline: true
|
||||||
@@ -34,4 +35,4 @@ domains:
|
|||||||
- "mig.{{ primary_domain }}"
|
- "mig.{{ primary_domain }}"
|
||||||
aliases:
|
aliases:
|
||||||
- "meta-infinite-graph.{{ primary_domain }}"
|
- "meta-infinite-graph.{{ primary_domain }}"
|
||||||
build_data: true # Enables the building of the meta data which the graph requiers
|
build_data: true # Enables the building of the meta data which the graph requiers
|
||||||
|
@@ -4,6 +4,7 @@ features:
|
|||||||
oidc: true
|
oidc: true
|
||||||
matomo: true
|
matomo: true
|
||||||
port-ui-desktop: true
|
port-ui-desktop: true
|
||||||
|
universal_logout: true
|
||||||
csp:
|
csp:
|
||||||
flags:
|
flags:
|
||||||
script-src-elem:
|
script-src-elem:
|
||||||
@@ -22,4 +23,4 @@ docker:
|
|||||||
mobilizon:
|
mobilizon:
|
||||||
image: "docker.io/framasoft/mobilizon"
|
image: "docker.io/framasoft/mobilizon"
|
||||||
name: "mobilizon"
|
name: "mobilizon"
|
||||||
version: ""
|
version: ""
|
||||||
|
@@ -5,6 +5,7 @@ features:
|
|||||||
port-ui-desktop: true
|
port-ui-desktop: true
|
||||||
central_database: true
|
central_database: true
|
||||||
oidc: true
|
oidc: true
|
||||||
|
universal_logout: true
|
||||||
csp:
|
csp:
|
||||||
flags:
|
flags:
|
||||||
script-src-elem:
|
script-src-elem:
|
||||||
@@ -35,4 +36,4 @@ docker:
|
|||||||
volumes:
|
volumes:
|
||||||
data: moodle_data
|
data: moodle_data
|
||||||
code: moodle_code
|
code: moodle_code
|
||||||
|
|
||||||
|
@@ -2,8 +2,9 @@
|
|||||||
features:
|
features:
|
||||||
matomo: true
|
matomo: true
|
||||||
css: true
|
css: true
|
||||||
port-ui-desktop: true
|
port-ui-desktop: true
|
||||||
central_database: true
|
central_database: true
|
||||||
|
universal_logout: true
|
||||||
docker:
|
docker:
|
||||||
services:
|
services:
|
||||||
database:
|
database:
|
||||||
@@ -13,4 +14,4 @@ docker:
|
|||||||
version: "latest"
|
version: "latest"
|
||||||
name: "mybb"
|
name: "mybb"
|
||||||
volumes:
|
volumes:
|
||||||
data: "mybb_data"
|
data: "mybb_data"
|
||||||
|
@@ -1,8 +1,8 @@
|
|||||||
features:
|
features:
|
||||||
matomo: true
|
matomo: true
|
||||||
css: true
|
css: true
|
||||||
port-ui-desktop: true
|
port-ui-desktop: true
|
||||||
|
universal_logout: false
|
||||||
csp:
|
csp:
|
||||||
whitelist:
|
whitelist:
|
||||||
script-src-elem:
|
script-src-elem:
|
||||||
@@ -14,6 +14,8 @@ csp:
|
|||||||
- https://cdn.jsdelivr.net
|
- https://cdn.jsdelivr.net
|
||||||
font-src:
|
font-src:
|
||||||
- https://cdnjs.cloudflare.com
|
- https://cdnjs.cloudflare.com
|
||||||
|
frame-src:
|
||||||
|
- "{{ web_protocol }}://*.{{primary_domain}}" # Makes sense that all of the website content is available in the navigator
|
||||||
flags:
|
flags:
|
||||||
style-src:
|
style-src:
|
||||||
unsafe-inline: true
|
unsafe-inline: true
|
||||||
@@ -23,4 +25,4 @@ csp:
|
|||||||
unsafe-inline: true
|
unsafe-inline: true
|
||||||
domains:
|
domains:
|
||||||
canonical:
|
canonical:
|
||||||
- "slides.{{ primary_domain }}"
|
- "slides.{{ primary_domain }}"
|
||||||
|
@@ -59,6 +59,7 @@ features:
|
|||||||
ldap: true
|
ldap: true
|
||||||
oidc: true
|
oidc: true
|
||||||
central_database: true
|
central_database: true
|
||||||
|
universal_logout: true
|
||||||
default_quota: '1000000000' # Quota to assign if no quota is specified in the OIDC response (bytes)
|
default_quota: '1000000000' # Quota to assign if no quota is specified in the OIDC response (bytes)
|
||||||
legacy_login_mask:
|
legacy_login_mask:
|
||||||
enabled: False # If true, then legacy login mask is shown. Otherwise just SSO
|
enabled: False # If true, then legacy login mask is shown. Otherwise just SSO
|
||||||
@@ -258,4 +259,4 @@ plugins:
|
|||||||
- sociallogin
|
- sociallogin
|
||||||
whiteboard:
|
whiteboard:
|
||||||
# Nextcloud Whiteboard: provides a collaborative drawing and brainstorming tool (https://apps.nextcloud.com/apps/whiteboard)
|
# Nextcloud Whiteboard: provides a collaborative drawing and brainstorming tool (https://apps.nextcloud.com/apps/whiteboard)
|
||||||
enabled: true
|
enabled: true
|
||||||
|
@@ -4,4 +4,5 @@ allowed_roles: "admin" # Restrict it default to admin r
|
|||||||
features:
|
features:
|
||||||
matomo: true
|
matomo: true
|
||||||
css: true
|
css: true
|
||||||
port-ui-desktop: false
|
port-ui-desktop: false
|
||||||
|
universal_logout: true
|
||||||
|
@@ -17,6 +17,7 @@ features:
|
|||||||
ldap: true
|
ldap: true
|
||||||
central_database: true
|
central_database: true
|
||||||
oauth2: true
|
oauth2: true
|
||||||
|
universal_logout: true
|
||||||
csp:
|
csp:
|
||||||
flags:
|
flags:
|
||||||
script-src-elem:
|
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
|
version: "" # If need a specific memcached version you have to define it here, otherwise the version from svc-db-memcached will be used
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
data: "openproject_data"
|
data: "openproject_data"
|
||||||
|
@@ -4,6 +4,7 @@ features:
|
|||||||
port-ui-desktop: true
|
port-ui-desktop: true
|
||||||
central_database: true
|
central_database: true
|
||||||
oidc: true
|
oidc: true
|
||||||
|
universal_logout: true
|
||||||
csp:
|
csp:
|
||||||
flags:
|
flags:
|
||||||
script-src-elem:
|
script-src-elem:
|
||||||
@@ -37,4 +38,4 @@ docker:
|
|||||||
backup:
|
backup:
|
||||||
no_stop_required: true
|
no_stop_required: true
|
||||||
volumes:
|
volumes:
|
||||||
data: peertube_data
|
data: peertube_data
|
||||||
|
@@ -9,9 +9,10 @@ oauth2_proxy:
|
|||||||
features:
|
features:
|
||||||
matomo: true
|
matomo: true
|
||||||
css: true
|
css: true
|
||||||
port-ui-desktop: true
|
port-ui-desktop: true
|
||||||
central_database: true
|
central_database: true
|
||||||
oauth2: true
|
oauth2: true
|
||||||
|
universal_logout: true
|
||||||
csp:
|
csp:
|
||||||
flags:
|
flags:
|
||||||
style-src:
|
style-src:
|
||||||
@@ -24,4 +25,4 @@ csp:
|
|||||||
docker:
|
docker:
|
||||||
services:
|
services:
|
||||||
database:
|
database:
|
||||||
enabled: true
|
enabled: true
|
||||||
|
@@ -7,6 +7,7 @@ oauth2_proxy:
|
|||||||
features:
|
features:
|
||||||
matomo: true
|
matomo: true
|
||||||
css: true
|
css: true
|
||||||
port-ui-desktop: true
|
port-ui-desktop: true
|
||||||
ldap: true
|
ldap: true
|
||||||
oauth2: true
|
oauth2: true
|
||||||
|
universal_logout: true
|
||||||
|
@@ -11,6 +11,7 @@ features:
|
|||||||
# it's anyhow not so enduser relevant, so it can be kept like this
|
# it's anyhow not so enduser relevant, so it can be kept like this
|
||||||
central_database: true
|
central_database: true
|
||||||
oauth2: true
|
oauth2: true
|
||||||
|
universal_logout: true
|
||||||
csp:
|
csp:
|
||||||
flags:
|
flags:
|
||||||
style-src:
|
style-src:
|
||||||
|
@@ -2,9 +2,10 @@ titel: "Pictures on {{primary_domain}}"
|
|||||||
features:
|
features:
|
||||||
matomo: true
|
matomo: true
|
||||||
css: false # Needs to be reactivated
|
css: false # Needs to be reactivated
|
||||||
port-ui-desktop: true
|
port-ui-desktop: true
|
||||||
central_database: true
|
central_database: true
|
||||||
oidc: true
|
oidc: true
|
||||||
|
universal_logout: true
|
||||||
csp:
|
csp:
|
||||||
flags:
|
flags:
|
||||||
script-src:
|
script-src:
|
||||||
@@ -15,6 +16,9 @@ csp:
|
|||||||
unsafe-eval: true
|
unsafe-eval: true
|
||||||
style-src:
|
style-src:
|
||||||
unsafe-inline: true
|
unsafe-inline: true
|
||||||
|
whitelist:
|
||||||
|
frame-ancestors:
|
||||||
|
- "*"
|
||||||
domains:
|
domains:
|
||||||
canonical:
|
canonical:
|
||||||
- "picture.{{ primary_domain }}"
|
- "picture.{{ primary_domain }}"
|
||||||
@@ -27,7 +31,7 @@ docker:
|
|||||||
database:
|
database:
|
||||||
enabled: true
|
enabled: true
|
||||||
pixelfed:
|
pixelfed:
|
||||||
image: "zknt/pixelfed"
|
image: "zknt/pixelfed"
|
||||||
version: "latest"
|
version: "latest"
|
||||||
name: "pixelfed"
|
name: "pixelfed"
|
||||||
backup:
|
backup:
|
||||||
|
@@ -4,6 +4,7 @@ features:
|
|||||||
port-ui-desktop: false
|
port-ui-desktop: false
|
||||||
simpleicons: true # Activate Brand Icons for your groups
|
simpleicons: true # Activate Brand Icons for your groups
|
||||||
javascript: true # Necessary for URL sync
|
javascript: true # Necessary for URL sync
|
||||||
|
universal_logout: false # Doesn't have own user data. Just a frame.
|
||||||
csp:
|
csp:
|
||||||
whitelist:
|
whitelist:
|
||||||
script-src-elem:
|
script-src-elem:
|
||||||
|
@@ -17,6 +17,7 @@ features:
|
|||||||
recaptcha: false # Enable ReCaptcha
|
recaptcha: false # Enable ReCaptcha
|
||||||
oauth2: false # Enable the OAuth2-Proy
|
oauth2: false # Enable the OAuth2-Proy
|
||||||
javascript: false # Enables the custom JS in the javascript.js.j2 file
|
javascript: false # Enables the custom JS in the javascript.js.j2 file
|
||||||
|
universal_logout: true
|
||||||
csp:
|
csp:
|
||||||
whitelist: {} # URL's which should be whitelisted
|
whitelist: {} # URL's which should be whitelisted
|
||||||
flags: {} # Flags which should be set
|
flags: {} # Flags which should be set
|
||||||
|
@@ -1,3 +1,5 @@
|
|||||||
|
features:
|
||||||
|
universal_logout: false
|
||||||
domains:
|
domains:
|
||||||
canonical:
|
canonical:
|
||||||
- "wheel.{{ primary_domain }}"
|
- "wheel.{{ primary_domain }}"
|
||||||
|
@@ -5,6 +5,7 @@ features:
|
|||||||
central_database: true
|
central_database: true
|
||||||
ldap: true
|
ldap: true
|
||||||
oauth2: true
|
oauth2: true
|
||||||
|
universal_logout: true
|
||||||
domains:
|
domains:
|
||||||
canonical:
|
canonical:
|
||||||
- "inventory.{{ primary_domain }}"
|
- "inventory.{{ primary_domain }}"
|
||||||
@@ -38,4 +39,4 @@ docker:
|
|||||||
image: "grokability/snipe-it"
|
image: "grokability/snipe-it"
|
||||||
volumes:
|
volumes:
|
||||||
data: "snipe-it_data"
|
data: "snipe-it_data"
|
||||||
|
|
||||||
|
@@ -2,6 +2,7 @@ features:
|
|||||||
matomo: true
|
matomo: true
|
||||||
css: true
|
css: true
|
||||||
port-ui-desktop: true
|
port-ui-desktop: true
|
||||||
|
universal_logout: false
|
||||||
csp:
|
csp:
|
||||||
flags:
|
flags:
|
||||||
script-src:
|
script-src:
|
||||||
|
@@ -1,3 +1,6 @@
|
|||||||
|
|
||||||
|
features:
|
||||||
|
universal_logout: false # Role is not enabled until then keep it false
|
||||||
# syncope:
|
# syncope:
|
||||||
# version: "latest"
|
# version: "latest"
|
||||||
# credentials:
|
# credentials:
|
||||||
@@ -9,4 +12,4 @@
|
|||||||
# password: "{{ users.administrator.password }}"
|
# password: "{{ users.administrator.password }}"
|
||||||
# users:
|
# users:
|
||||||
# administrator:
|
# administrator:
|
||||||
# username: "{{ users.administrator.username }}"
|
# username: "{{ users.administrator.username }}"
|
||||||
|
@@ -11,6 +11,7 @@ features:
|
|||||||
port-ui-desktop: true
|
port-ui-desktop: true
|
||||||
oidc: false
|
oidc: false
|
||||||
central_database: true
|
central_database: true
|
||||||
|
universal_logout: true
|
||||||
docker:
|
docker:
|
||||||
services:
|
services:
|
||||||
database:
|
database:
|
||||||
@@ -28,4 +29,4 @@ csp:
|
|||||||
unsafe-eval: true
|
unsafe-eval: true
|
||||||
domains:
|
domains:
|
||||||
canonical:
|
canonical:
|
||||||
- "kanban.{{ primary_domain }}"
|
- "kanban.{{ primary_domain }}"
|
||||||
|
@@ -9,9 +9,10 @@ plugins:
|
|||||||
features:
|
features:
|
||||||
matomo: true
|
matomo: true
|
||||||
css: false
|
css: false
|
||||||
port-ui-desktop: true
|
port-ui-desktop: true
|
||||||
oidc: true
|
oidc: true
|
||||||
central_database: true
|
central_database: true
|
||||||
|
universal_logout: true
|
||||||
csp:
|
csp:
|
||||||
flags:
|
flags:
|
||||||
style-src:
|
style-src:
|
||||||
|
@@ -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
|
||||||
|
@@ -12,6 +12,7 @@ features:
|
|||||||
port-ui-desktop: true
|
port-ui-desktop: true
|
||||||
central_database: true
|
central_database: true
|
||||||
oauth2: true
|
oauth2: true
|
||||||
|
universal_logout: true
|
||||||
domains:
|
domains:
|
||||||
canonical:
|
canonical:
|
||||||
- "s.{{ primary_domain }}"
|
- "s.{{ primary_domain }}"
|
||||||
@@ -24,4 +25,4 @@ docker:
|
|||||||
yourls:
|
yourls:
|
||||||
version: "latest"
|
version: "latest"
|
||||||
name: "yourls"
|
name: "yourls"
|
||||||
image: "yourls"
|
image: "yourls"
|
||||||
|
35
roles/web-svc-logout/README.md
Normal file
35
roles/web-svc-logout/README.md
Normal file
@@ -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).*
|
2
roles/web-svc-logout/Todo.md
Normal file
2
roles/web-svc-logout/Todo.md
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
# Todos
|
||||||
|
- solve loading of domains which are not in group names, but declared via dependencies
|
0
roles/web-svc-logout/__init__.py
Normal file
0
roles/web-svc-logout/__init__.py
Normal file
25
roles/web-svc-logout/config/main.yml
Normal file
25
roles/web-svc-logout/config/main.yml
Normal file
@@ -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] >>"
|
||||||
|
|
0
roles/web-svc-logout/filter_plugins/__init__.py
Normal file
0
roles/web-svc-logout/filter_plugins/__init__.py
Normal file
49
roles/web-svc-logout/filter_plugins/domain_filters.py
Normal file
49
roles/web-svc-logout/filter_plugins/domain_filters.py
Normal file
@@ -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}")
|
37
roles/web-svc-logout/meta/main.yml
Normal file
37
roles/web-svc-logout/meta/main.yml
Normal file
@@ -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"
|
18
roles/web-svc-logout/tasks/main.yml
Normal file
18
roles/web-svc-logout/tasks/main.yml
Normal file
@@ -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
|
14
roles/web-svc-logout/templates/docker-compose.yml.j2
Normal file
14
roles/web-svc-logout/templates/docker-compose.yml.j2
Normal file
@@ -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' %}
|
14
roles/web-svc-logout/templates/env.j2
Normal file
14
roles/web-svc-logout/templates/env.j2
Normal file
@@ -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
|
25
roles/web-svc-logout/templates/logout-proxy.conf.j2
Normal file
25
roles/web-svc-logout/templates/logout-proxy.conf.j2
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
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;
|
||||||
|
|
||||||
|
# Disable caching absolutely
|
||||||
|
add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0" always;
|
||||||
|
add_header Pragma "no-cache" always;
|
||||||
|
add_header Expires "0" always;
|
||||||
|
|
||||||
|
# Handle preflight
|
||||||
|
if ($request_method = OPTIONS) {
|
||||||
|
return 204;
|
||||||
|
}
|
||||||
|
}
|
15
roles/web-svc-logout/vars/main.yml
Normal file
15
roles/web-svc-logout/vars/main.yml
Normal file
@@ -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(',')
|
||||||
|
}}
|
@@ -23,7 +23,8 @@ features:
|
|||||||
central_database: false # Enable Central Database Network
|
central_database: false # Enable Central Database Network
|
||||||
recaptcha: false # Enable ReCaptcha
|
recaptcha: false # Enable ReCaptcha
|
||||||
oauth2: false # Enable the OAuth2-Proy
|
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:
|
csp:
|
||||||
whitelist: # URL's which should be whitelisted
|
whitelist: # URL's which should be whitelisted
|
||||||
script-src-elem: []
|
script-src-elem: []
|
||||||
|
44
tests/integration/test_universal_logout.py
Normal file
44
tests/integration/test_universal_logout.py
Normal file
@@ -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()
|
0
tests/unit/roles/web-svc-logout/__init__.py
Normal file
0
tests/unit/roles/web-svc-logout/__init__.py
Normal file
@@ -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()
|
Reference in New Issue
Block a user