Compare commits

...

7 Commits

66 changed files with 417 additions and 366 deletions

View File

@ -1,17 +1,26 @@
ROLES_DIR=./roles ROLES_DIR := ./roles
OUTPUT=./group_vars/all/11_applications.yml APPLICATIONS_OUT := ./group_vars/all/11_applications.yml
SCRIPT=./cli/generate_defaults_applications.py APPLICATIONS_SCRIPT := ./cli/generate_defaults_applications.py
INCLUDES_OUT := ./tasks/include-docker-roles.yml
INCLUDES_SCRIPT := ./cli/generate_role_includes.py
.PHONY: build install test
build: build:
@echo "🔧 Generating $(OUTPUT) from roles in $(ROLES_DIR)..." @echo "🔧 Generating applications defaults → $(APPLICATIONS_OUT) from roles in $(ROLES_DIR)"
@mkdir -p $(dir $(OUTPUT)) @mkdir -p $(dir $(APPLICATIONS_OUT))
python3 $(SCRIPT) --roles-dir $(ROLES_DIR) --output-file $(OUTPUT) python3 $(APPLICATIONS_SCRIPT) --roles-dir $(ROLES_DIR) --output-file $(APPLICATIONS_OUT)
@echo "✅ Output written to $(OUTPUT)" @echo "✅ Applications defaults written to $(APPLICATIONS_OUT)\n"
@echo "🔧 Generating Docker role includes → $(INCLUDES_OUT)"
@mkdir -p $(dir $(INCLUDES_OUT))
python3 $(INCLUDES_SCRIPT) $(ROLES_DIR) -o $(INCLUDES_OUT) -p docker-
@echo "✅ Docker role includes written to $(INCLUDES_OUT)"
install: build install: build
@echo "⚙️ Install complete."
test: test:
@echo "Executing Unit Tests:" @echo "🧪 Running Unit Tests..."
python -m unittest discover -s tests/unit python -m unittest discover -s tests/unit
@echo "Executing Integration Tests:" @echo "🔬 Running Integration Tests..."
python -m unittest discover -s tests/integration python -m unittest discover -s tests/integration

View File

@ -0,0 +1,79 @@
import os
import argparse
import yaml
def find_roles(roles_dir, prefix=None):
"""
Yield absolute paths of role directories under roles_dir.
Only include roles whose directory name starts with prefix (if given) and contain vars/main.yml.
"""
for entry in os.listdir(roles_dir):
if prefix and not entry.startswith(prefix):
continue
path = os.path.join(roles_dir, entry)
vars_file = os.path.join(path, 'vars', 'main.yml')
if os.path.isdir(path) and os.path.isfile(vars_file):
yield path, vars_file
def load_application_id(vars_file):
"""
Load the vars/main.yml and return the value of application_id key.
Returns None if not found.
"""
with open(vars_file, 'r') as f:
data = yaml.safe_load(f) or {}
return data.get('application_id')
def generate_playbook_entries(roles_dir, prefix=None):
entries = []
for role_path, vars_file in find_roles(roles_dir, prefix):
app_id = load_application_id(vars_file)
if not app_id:
continue
# Derive role name from directory name
role_name = os.path.basename(role_path)
# entry text
entry = (
f"- name: setup {app_id}\n"
f" when: (\"{app_id}\" in group_names)\n"
f" include_role:\n"
f" name: {role_name}\n"
)
entries.append(entry)
return entries
def main():
parser = argparse.ArgumentParser(
description='Generate an Ansible playbook include file from Docker roles and application_ids.'
)
parser.add_argument(
'roles_dir',
help='Path to directory containing role folders'
)
parser.add_argument(
'-p', '--prefix',
help='Only include roles whose names start with this prefix (e.g. docker-, client-)',
default=None
)
parser.add_argument(
'-o', '--output',
help='Output file path (default: stdout)',
default=None
)
args = parser.parse_args()
entries = generate_playbook_entries(args.roles_dir, args.prefix)
output = ''.join(entries)
if args.output:
with open(args.output, 'w') as f:
f.write(output)
print(f"Playbook entries written to {args.output}")
else:
print(output)
if __name__ == '__main__':
main()

View File

@ -21,7 +21,7 @@ defaults_applications:
features: # Version of the service features: # Version of the service
matomo: true # Enable Matomo tracking for analytics matomo: true # Enable Matomo tracking for analytics
css: true # Enable or disable global CSS styling css: true # Enable or disable global CSS styling
iframe: false # Allow embedding the landing page in an iframe (if true) portfolio_iframe: false # Allow embedding the landing page in an iframe (if true)
database: true # Enable central database integration database: true # Enable central database integration
ldap: true # Enable ldap integration ldap: true # Enable ldap integration
oauth2: true # Enable oauth2 proxy oauth2: true # Enable oauth2 proxy

View File

@ -0,0 +1,2 @@
from pkgutil import extend_path
__path__ = extend_path(__path__, __name__)

View File

@ -113,6 +113,13 @@ class FilterModule(object):
if matomo_domain: if matomo_domain:
tokens.append(f"{web_protocol}://{matomo_domain}") tokens.append(f"{web_protocol}://{matomo_domain}")
# ReCaptcha integration: allow loading scripts from Google if feature enabled
if (
self.is_feature_enabled(applications, 'recaptcha', application_id)
and directive == 'script-src'
):
tokens.append('https://www.google.com')
# whitelist # whitelist
tokens += self.get_csp_whitelist(applications, application_id, directive) tokens += self.get_csp_whitelist(applications, application_id, directive)

View File

@ -1,3 +1,4 @@
CYMAIS_ENVIRONMENT: "production"
HOST_CURRENCY: "EUR" HOST_CURRENCY: "EUR"
HOST_TIMEZONE: "UTC" HOST_TIMEZONE: "UTC"

View File

@ -3,6 +3,7 @@
import argparse import argparse
import subprocess import subprocess
import os import os
import datetime
def run_ansible_vault(action, filename, password_file): def run_ansible_vault(action, filename, password_file):
"""Execute an ansible-vault command with the specified action on a file.""" """Execute an ansible-vault command with the specified action on a file."""
@ -10,6 +11,8 @@ def run_ansible_vault(action, filename, password_file):
subprocess.run(cmd, check=True) subprocess.run(cmd, check=True)
def run_ansible_playbook(inventory: str, playbook: str, modes: dict, limit: str = None, password_file: str = None, verbose: int = 0, skip_tests: bool = False): def run_ansible_playbook(inventory: str, playbook: str, modes: dict, limit: str = None, password_file: str = None, verbose: int = 0, skip_tests: bool = False):
start_time = datetime.datetime.now().isoformat()
print(f"\n▶️ Script started at: {start_time}\n")
print("\n🛠️ Building project (make build)...\n") print("\n🛠️ Building project (make build)...\n")
subprocess.run(["make", "build"], check=True) subprocess.run(["make", "build"], check=True)
@ -39,6 +42,8 @@ def run_ansible_playbook(inventory: str, playbook: str, modes: dict, limit: str
print("\n🚀 Launching Ansible Playbook...\n") print("\n🚀 Launching Ansible Playbook...\n")
subprocess.run(cmd, check=True) subprocess.run(cmd, check=True)
end_time = datetime.datetime.now().isoformat()
print(f"\n✅ Script ended at: {end_time}\n")
def main(): def main():
# Change to script dir to execute all folders relative to their # Change to script dir to execute all folders relative to their
script_dir = os.path.dirname(os.path.realpath(__file__)) script_dir = os.path.dirname(os.path.realpath(__file__))

View File

@ -5,7 +5,7 @@ setup_admin_email: "{{users.administrator.email}}"
features: features:
matomo: true matomo: true
css: true css: true
landingpage_iframe: false portfolio_iframe: false
central_database: true central_database: true
credentials: credentials:
# database_password: Needs to be defined in inventory file # database_password: Needs to be defined in inventory file

View File

@ -5,5 +5,5 @@ credentials:
features: features:
matomo: true matomo: true
css: true css: true
landingpage_iframe: false portfolio_iframe: false
central_database: true central_database: true

View File

@ -2,5 +2,5 @@ version: "latest"
features: features:
matomo: true matomo: true
css: true css: true
landingpage_iframe: true portfolio_iframe: true
central_database: true central_database: true

View File

@ -15,7 +15,7 @@ urls:
features: features:
matomo: true matomo: true
css: true css: true
landingpage_iframe: false portfolio_iframe: false
ldap: false ldap: false
oidc: true oidc: true
central_database: false central_database: false

View File

@ -10,5 +10,5 @@ credentials:
features: features:
matomo: true matomo: true
css: true css: true
landingpage_iframe: true portfolio_iframe: true
central_database: true central_database: true

View File

@ -5,7 +5,7 @@ credentials:
features: features:
matomo: true matomo: true
css: true css: true
landingpage_iframe: false portfolio_iframe: false
oidc: true oidc: true
central_database: true central_database: true
csp: csp:

View File

@ -8,7 +8,7 @@ credentials:
features: features:
matomo: true matomo: true
css: false css: false
landingpage_iframe: false portfolio_iframe: false
ldap: false ldap: false
oidc: true oidc: true
central_database: true central_database: true

View File

@ -2,6 +2,6 @@ version: "latest"
features: features:
matomo: true matomo: true
css: true css: true
landingpage_iframe: true portfolio_iframe: true
oidc: true oidc: true
central_database: true central_database: true

View File

@ -2,7 +2,7 @@ version: "1.4.0"
features: features:
matomo: true matomo: true
css: true css: true
landingpage_iframe: true portfolio_iframe: true
ldap: true ldap: true
central_database: true central_database: true
credentials: credentials:

View File

@ -12,7 +12,7 @@ SSH_PORT={{ports.public.ssh[application_id]}}
SSH_LISTEN_PORT=22 SSH_LISTEN_PORT=22
DOMAIN={{domains[application_id]}} DOMAIN={{domains[application_id]}}
SSH_DOMAIN={{domains[application_id]}} SSH_DOMAIN={{domains[application_id]}}
RUN_MODE="{{run_mode}}" RUN_MODE="{{ 'dev' if (CYMAIS_ENVIRONMENT | lower) == 'development' else 'prod' }}"
ROOT_URL="{{ web_protocol }}://{{domains[application_id]}}/" ROOT_URL="{{ web_protocol }}://{{domains[application_id]}}/"
# Mail Configuration # Mail Configuration

View File

@ -7,7 +7,7 @@ configuration:
features: features:
matomo: true matomo: true
css: true css: true
landingpage_iframe: true portfolio_iframe: true
central_database: true central_database: true
csp: csp:
flags: flags:

View File

@ -2,5 +2,5 @@ version: "latest"
features: features:
matomo: true matomo: true
css: true css: true
landingpage_iframe: true portfolio_iframe: true
central_database: true central_database: true

View File

@ -2,4 +2,4 @@ version: "latest"
features: features:
matomo: true matomo: true
css: true css: true
landingpage_iframe: true portfolio_iframe: true

View File

@ -4,12 +4,16 @@ users:
username: "{{users.administrator.username}}" # Administrator Username for Keycloak username: "{{users.administrator.username}}" # Administrator Username for Keycloak
import_realm: True # If True realm will be imported. If false skip. import_realm: True # If True realm will be imported. If false skip.
credentials: credentials:
# database_password: # Needs to be defined in inventory file
# administrator_password: # Needs to be defined in inventory file
features: features:
matomo: true matomo: true
css: true css: true
landingpage_iframe: true portfolio_iframe: true
ldap: true ldap: true
central_database: true central_database: true
recaptcha: true recaptcha: true
csp:
flags:
script-src:
unsafe-inline: true
style-src:
unsafe-inline: true

View File

@ -8,7 +8,7 @@ credentials:
features: features:
matomo: true matomo: true
css: true css: true
landingpage_iframe: true portfolio_iframe: true
ldap: true ldap: true
central_database: false central_database: false
oauth2: false oauth2: false

View File

@ -6,6 +6,6 @@ version: "latest" # Docker Image
features: features:
matomo: true matomo: true
css: true css: true
landingpage_iframe: true portfolio_iframe: true
central_database: true central_database: true
oidc: true oidc: true

View File

@ -15,6 +15,6 @@ credentials:
features: features:
matomo: true matomo: true
css: true css: true
landingpage_iframe: false # Deactivated mailu iframe loading until keycloak supports it portfolio_iframe: false # 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

View File

@ -14,6 +14,6 @@ credentials:
features: features:
matomo: true matomo: true
css: true css: true
landingpage_iframe: false portfolio_iframe: false
oidc: true oidc: true
central_database: true central_database: true

View File

@ -2,7 +2,7 @@ version: "latest"
features: features:
matomo: true matomo: true
css: false css: false
landingpage_iframe: false portfolio_iframe: false
central_database: true central_database: true
oauth2: false oauth2: false
csp: csp:

View File

@ -0,0 +1,5 @@
# Todo
- Enable Whatsapp by default
- Enable Telegram by default
- Enable Slack by default
- Enable ChatGPT by default

View File

@ -0,0 +1,2 @@
from pkgutil import extend_path
__path__ = extend_path(__path__, __name__)

View File

@ -0,0 +1,13 @@
def filter_enabled_bridges(bridges, plugins):
"""
Return only those bridge definitions whose 'bridge_name' is set to True in plugins.
:param bridges: list of dicts, each with a 'bridge_name' key
:param plugins: dict mapping bridge_name to a boolean
"""
return [b for b in bridges if plugins.get(b['bridge_name'], False)]
class FilterModule(object):
def filters(self):
return {
'filter_enabled_bridges': filter_enabled_bridges,
}

View File

@ -1,4 +1,13 @@
--- ---
- name: Load bridges configuration
include_vars:
file: "bridges.yml"
- name: Filter enabled bridges and register as fact
set_fact:
bridges: "{{ bridges_configuration | filter_enabled_bridges(applications[application_id].plugins) }}"
changed_when: false
- name: "include docker-central-database" - name: "include docker-central-database"
include_role: include_role:
name: docker-central-database name: docker-central-database

View File

@ -25,7 +25,11 @@ services:
interval: 1m interval: 1m
timeout: 10s timeout: 10s
retries: 3 retries: 3
{% if bridges | bool %}
{% include 'templates/docker/container/depends-on-also-database.yml.j2' %} {% include 'templates/docker/container/depends-on-also-database.yml.j2' %}
{% else %}
{% include 'templates/docker/container/depends-on-just-database.yml.j2' %}
{% endif %}
{% for item in bridges %} {% for item in bridges %}
mautrix-{{item.bridge_name}}: mautrix-{{item.bridge_name}}:
condition: service_healthy condition: service_healthy
@ -61,47 +65,47 @@ services:
retries: 3 retries: 3
{% include 'templates/docker/container/networks.yml.j2' %} {% include 'templates/docker/container/networks.yml.j2' %}
{% endfor %} {% endfor %}
# Deactivated chatgpt. {% if applications[application_id] | bool %}
# @todo needs to be reactivated as soon as bug is found matrix-chatgpt-bot:
# matrix-chatgpt-bot: restart: {{docker_restart_policy}}
# restart: {{docker_restart_policy}} container_name: matrix-chatgpt
# container_name: matrix-chatgpt image: ghcr.io/matrixgpt/matrix-chatgpt-bot:latest
# image: ghcr.io/matrixgpt/matrix-chatgpt-bot:latest volumes:
# volumes: - chatgpt_data:/storage
# - chatgpt_data:/storage environment:
# environment: OPENAI_API_KEY: '{{applications[application_id].credentials.chatgpt_bridge_openai_api_key}}'
# OPENAI_API_KEY: '{{applications[application_id].credentials.chatgpt_bridge_openai_api_key}}' # Uncomment the next two lines if you are using Azure OpenAI API
# # Uncomment the next two lines if you are using Azure OpenAI API # OPENAI_AZURE: 'false'
# # OPENAI_AZURE: 'false' # CHATGPT_REVERSE_PROXY: 'your-completion-endpoint-here'
# # CHATGPT_REVERSE_PROXY: 'your-completion-endpoint-here' CHATGPT_CONTEXT: 'thread'
# CHATGPT_CONTEXT: 'thread' CHATGPT_API_MODEL: 'gpt-3.5-turbo'
# CHATGPT_API_MODEL: 'gpt-3.5-turbo' # Uncomment and edit the next line if needed
# # Uncomment and edit the next line if needed # CHATGPT_PROMPT_PREFIX: 'Instructions:\nYou are ChatGPT, a large language model trained by OpenAI.'
# # CHATGPT_PROMPT_PREFIX: 'Instructions:\nYou are ChatGPT, a large language model trained by OpenAI.' # CHATGPT_IGNORE_MEDIA: 'false'
# # CHATGPT_IGNORE_MEDIA: 'false' CHATGPT_REVERSE_PROXY: 'https://api.openai.com/v1/chat/completions'
# CHATGPT_REVERSE_PROXY: 'https://api.openai.com/v1/chat/completions' # Uncomment and edit the next line if needed
# # Uncomment and edit the next line if needed # CHATGPT_TEMPERATURE: '0.8'
# # CHATGPT_TEMPERATURE: '0.8' # Uncomment and edit the next line if needed
# # Uncomment and edit the next line if needed #CHATGPT_MAX_CONTEXT_TOKENS: '4097'
# #CHATGPT_MAX_CONTEXT_TOKENS: '4097' CHATGPT_MAX_PROMPT_TOKENS: '3000'
# CHATGPT_MAX_PROMPT_TOKENS: '3000' KEYV_BACKEND: 'file'
# KEYV_BACKEND: 'file' KEYV_URL: ''
# KEYV_URL: '' KEYV_BOT_ENCRYPTION: 'false'
# KEYV_BOT_ENCRYPTION: 'false' KEYV_BOT_STORAGE: 'true'
# KEYV_BOT_STORAGE: 'true' MATRIX_HOMESERVER_URL: 'https://{{domains.synapse}}'
# MATRIX_HOMESERVER_URL: 'https://{{domains.synapse}}' MATRIX_BOT_USERNAME: '@chatgptbot:{{applications.matrix.server_name}}'
# MATRIX_BOT_USERNAME: '@chatgptbot:{{applications.matrix.server_name}}' MATRIX_ACCESS_TOKEN: '{{ applications[application_id].credentials.chatgpt_bridge_access_token | default('') }}'
# MATRIX_ACCESS_TOKEN: '{{ applications[application_id].credentials.chatgpt_bridge_access_token | default('') }}' MATRIX_BOT_PASSWORD: '{{applications[application_id].credentials.chatgpt_bridge_user_password}}'
# MATRIX_BOT_PASSWORD: '{{applications[application_id].credentials.chatgpt_bridge_user_password}}' MATRIX_DEFAULT_PREFIX: '!chatgpt'
# MATRIX_DEFAULT_PREFIX: '!chatgpt' MATRIX_DEFAULT_PREFIX_REPLY: 'false'
# MATRIX_DEFAULT_PREFIX_REPLY: 'false' #MATRIX_BLACKLIST: ''
# #MATRIX_BLACKLIST: '' MATRIX_WHITELIST: ':{{applications.matrix.server_name}}'
# MATRIX_WHITELIST: ':{{applications.matrix.server_name}}' MATRIX_AUTOJOIN: 'true'
# MATRIX_AUTOJOIN: 'true' MATRIX_ENCRYPTION: 'true'
# MATRIX_ENCRYPTION: 'true' MATRIX_THREADS: 'true'
# MATRIX_THREADS: 'true' MATRIX_PREFIX_DM: 'false'
# MATRIX_PREFIX_DM: 'false' MATRIX_RICH_TEXT: 'true'
# MATRIX_RICH_TEXT: 'true' {% endif %}
{% include 'templates/docker/compose/volumes.yml.j2' %} {% include 'templates/docker/compose/volumes.yml.j2' %}
synapse_data: synapse_data:

View File

@ -45,7 +45,7 @@ email:
client_base_url: "{{domains.synapse}}" client_base_url: "{{domains.synapse}}"
validation_token_lifetime: 15m validation_token_lifetime: 15m
{% if applications[application_id].features.oidc | bool %} {% if applications | is_feature_enabled('oidc',application_id) %}
# @See https://matrix-org.github.io/synapse/latest/openid.html # @See https://matrix-org.github.io/synapse/latest/openid.html
oidc_providers: oidc_providers:
- idp_id: keycloak - idp_id: keycloak
@ -61,7 +61,9 @@ oidc_providers:
backchannel_logout_enabled: true backchannel_logout_enabled: true
{% endif %} {% endif %}
{% if bridges | bool %}
app_service_config_files: app_service_config_files:
{% for item in bridges %} {% for item in bridges %}
- {{registration_file_folder}}{{item.bridge_name}}.registration.yaml - {{registration_file_folder}}{{item.bridge_name}}.registration.yaml
{% endfor %} {% endfor %}
{% endif %}

View File

@ -0,0 +1,30 @@
bridges_configuration:
- database_password: "{{ applications[application_id].credentials.mautrix_whatsapp_bridge_database_password }}"
database_username: "mautrix_whatsapp_bridge"
database_name: "mautrix_whatsapp_bridge"
bridge_name: "whatsapp"
- database_password: "{{ applications[application_id].credentials.mautrix_telegram_bridge_database_password }}"
database_username: "mautrix_telegram_bridge"
database_name: "mautrix_telegram_bridge"
bridge_name: "telegram"
- database_password: "{{ applications[application_id].credentials.mautrix_signal_bridge_database_password }}"
database_username: "mautrix_signal_bridge"
database_name: "mautrix_signal_bridge"
bridge_name: "signal"
- database_password: "{{ applications[application_id].credentials.mautrix_slack_bridge_database_password }}"
database_username: "mautrix_slack_bridge"
database_name: "mautrix_slack_bridge"
bridge_name: "slack"
- database_password: "{{ applications[application_id].credentials.mautrix_facebook_bridge_database_password }}"
database_username: "mautrix_facebook_bridge"
database_name: "mautrix_facebook_bridge"
bridge_name: "facebook"
- database_password: "{{ applications[application_id].credentials.mautrix_instagram_bridge_database_password }}"
database_username: "mautrix_instagram_bridge"
database_name: "mautrix_instagram_bridge"
bridge_name: "instagram"

View File

@ -3,7 +3,6 @@ users:
administrator: administrator:
username: "{{users.administrator.username}}" # Accountname of the matrix admin username: "{{users.administrator.username}}" # Accountname of the matrix admin
playbook_tags: "setup-all,start" # For the initial update use: install-all,ensure-matrix-users-created,start playbook_tags: "setup-all,start" # For the initial update use: install-all,ensure-matrix-users-created,start
role: "compose" # Role to setup Matrix. Valid values: ansible, compose
server_name: "{{primary_domain}}" # Adress for the account names etc. server_name: "{{primary_domain}}" # Adress for the account names etc.
synapse: synapse:
version: "latest" version: "latest"
@ -13,8 +12,8 @@ setup: false # Set true in inventory
features: features:
matomo: true matomo: true
css: true css: true
landingpage_iframe: false portfolio_iframe: false
oidc: false # 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
csp: csp:
flags: flags:
@ -30,3 +29,14 @@ csp:
script-src: script-src:
- "{{ domains.synapse }}" - "{{ domains.synapse }}"
- "https://cdn.jsdelivr.net" - "https://cdn.jsdelivr.net"
plugins:
# You need to enable them in the inventory file
chatgpt: false
facebook: false
immesage: false
instagram: false
signal: false
slack: false
telegram: false
whatsapp: false

View File

@ -3,36 +3,3 @@ application_id: "matrix"
database_type: "postgres" database_type: "postgres"
registration_file_folder: "/data/" registration_file_folder: "/data/"
well_known_directory: "{{nginx.directories.data.well_known}}/matrix/" well_known_directory: "{{nginx.directories.data.well_known}}/matrix/"
bridges:
- database_password: "{{ applications[application_id].credentials.mautrix_whatsapp_bridge_database_password }}"
database_username: "mautrix_whatsapp_bridge"
database_name: "mautrix_whatsapp_bridge"
bridge_name: "whatsapp"
- database_password: "{{ applications[application_id].credentials.mautrix_telegram_bridge_database_password }}"
database_username: "mautrix_telegram_bridge"
database_name: "mautrix_telegram_bridge"
bridge_name: "telegram"
- database_password: "{{ applications[application_id].credentials.mautrix_signal_bridge_database_password }}"
database_username: "mautrix_signal_bridge"
database_name: "mautrix_signal_bridge"
bridge_name: "signal"
# Deactivated temporary, due to bug which is hard to find
# @todo Reactivate
# - database_password: "{{ applications[application_id].credentials.mautrix_slack_bridge_database_password }}"
# database_username: "mautrix_slack_bridge"
# database_name: "mautrix_slack_bridge"
# bridge_name: "slack"
- database_password: "{{ applications[application_id].credentials.mautrix_facebook_bridge_database_password }}"
database_username: "mautrix_facebook_bridge"
database_name: "mautrix_facebook_bridge"
bridge_name: "facebook"
- database_password: "{{ applications[application_id].credentials.mautrix_instagram_bridge_database_password }}"
database_username: "mautrix_instagram_bridge"
database_name: "mautrix_instagram_bridge"
bridge_name: "instagram"

View File

@ -6,8 +6,8 @@ users:
version: "latest" version: "latest"
features: features:
matomo: true matomo: true
css: true css: false
landingpage_iframe: false portfolio_iframe: false
central_database: true central_database: true
csp: csp:
flags: flags:
@ -20,4 +20,5 @@ csp:
font-src: font-src:
- "data:" - "data:"
- "blob:" - "blob:"
script-src:
- "https://cdn.jsdelivr.net" - "https://cdn.jsdelivr.net"

View File

@ -3,5 +3,5 @@ version: "latest"
features: features:
matomo: true matomo: true
css: true css: true
landingpage_iframe: false portfolio_iframe: false
central_database: true central_database: true

View File

@ -23,7 +23,7 @@ credentials:
features: features:
matomo: true matomo: true
css: true css: true
landingpage_iframe: false portfolio_iframe: false
ldap: true ldap: true
oidc: true oidc: true
central_database: true central_database: true

View File

@ -5,4 +5,4 @@ allowed_roles: admin
features: features:
matomo: true matomo: true
css: true css: true
landingpage_iframe: false portfolio_iframe: false

View File

@ -96,7 +96,7 @@
shell: > shell: >
docker compose exec web bash -c " docker compose exec web bash -c "
cd /app && cd /app &&
RAILS_ENV=production bundle exec rails runner \" RAILS_ENV={{ CYMAIS_ENVIRONMENT | lower }} bundle exec rails runner \"
user = User.find_by(mail: '{{ users.administrator.email }}'); user = User.find_by(mail: '{{ users.administrator.email }}');
if user.nil?; if user.nil?;
puts 'User with email {{ users.administrator.email }} not found.'; puts 'User with email {{ users.administrator.email }} not found.';

View File

@ -49,7 +49,7 @@
- name: Set settings in OpenProject - name: Set settings in OpenProject
shell: > shell: >
docker compose exec web bash -c "cd /app && docker compose exec web bash -c "cd /app &&
RAILS_ENV=production bundle exec rails runner \"Setting[:{{ item.key }}] = '{{ item.value }}'\"" RAILS_ENV={{ CYMAIS_ENVIRONMENT | lower }} bundle exec rails runner \"Setting[:{{ item.key }}] = '{{ item.value }}'\""
args: args:
chdir: "{{ docker_compose.directories.instance }}" chdir: "{{ docker_compose.directories.instance }}"
loop: "{{ openproject_rails_settings | dict2items }}" loop: "{{ openproject_rails_settings | dict2items }}"

View File

@ -9,7 +9,7 @@ ldap:
features: features:
matomo: true matomo: true
css: true css: true
landingpage_iframe: false portfolio_iframe: false
ldap: true ldap: true
central_database: true central_database: true
oauth2: true oauth2: true

View File

@ -2,7 +2,7 @@ version: "bookworm"
features: features:
matomo: true matomo: true
css: true css: true
landingpage_iframe: false portfolio_iframe: false
central_database: true central_database: true
csp: csp:
flags: flags:

View File

@ -10,6 +10,6 @@ oauth2_proxy:
features: features:
matomo: true matomo: true
css: true css: true
landingpage_iframe: false portfolio_iframe: false
central_database: true central_database: true
oauth2: true oauth2: true

View File

@ -5,6 +5,6 @@ oauth2_proxy:
features: features:
matomo: true matomo: true
css: true css: true
landingpage_iframe: false portfolio_iframe: false
ldap: true ldap: true
oauth2: true oauth2: true

View File

@ -6,7 +6,7 @@ oauth2_proxy:
features: features:
matomo: true matomo: true
css: false css: false
landingpage_iframe: false portfolio_iframe: false
central_database: true central_database: true
oauth2: true oauth2: true
hostname: central-mariadb hostname: central-mariadb

View File

@ -3,7 +3,7 @@ version: "latest"
features: features:
matomo: true matomo: true
css: true css: true
landingpage_iframe: false portfolio_iframe: false
central_database: true central_database: true
csp: csp:
flags: flags:

View File

@ -28,7 +28,7 @@ accounts:
class: fa-brands fa-mastodon class: fa-brands fa-mastodon
url: "{{ web_protocol }}://{{ service_provider.contact.mastodon.split('@')[2] }}/@{{ service_provider.contact.mastodon.split('@')[1] }}" url: "{{ web_protocol }}://{{ service_provider.contact.mastodon.split('@')[2] }}/@{{ service_provider.contact.mastodon.split('@')[1] }}"
identifier: "{{service_provider.contact.mastodon}}" identifier: "{{service_provider.contact.mastodon}}"
iframe: {{ applications | is_feature_enabled('landing_page_iframe','mastodon') }} iframe: {{ applications | is_feature_enabled('portfolio_iframe','mastodon') }}
{% endif %} {% endif %}
{% if service_provider.contact.bluesky is defined and service_provider.contact.bluesky != "" %} {% if service_provider.contact.bluesky is defined and service_provider.contact.bluesky != "" %}
@ -52,7 +52,7 @@ accounts:
class: fa-solid fa-camera class: fa-solid fa-camera
identifier: "{{service_provider.contact.pixelfed}}" identifier: "{{service_provider.contact.pixelfed}}"
url: "{{ web_protocol }}://{{ service_provider.contact.pixelfed.split('@')[2] }}/@{{ service_provider.contact.pixelfed.split('@')[1] }}" url: "{{ web_protocol }}://{{ service_provider.contact.pixelfed.split('@')[2] }}/@{{ service_provider.contact.pixelfed.split('@')[1] }}"
iframe: {{ applications | is_feature_enabled('landing_page_iframe','pixelfed') }} iframe: {{ applications | is_feature_enabled('portfolio_iframe','pixelfed') }}
{% endif %} {% endif %}
{% if service_provider.contact.peertube is defined and service_provider.contact.peertube != "" %} {% if service_provider.contact.peertube is defined and service_provider.contact.peertube != "" %}
@ -64,7 +64,7 @@ accounts:
class: fa-solid fa-video class: fa-solid fa-video
identifier: "{{service_provider.contact.peertube}}" identifier: "{{service_provider.contact.peertube}}"
url: "{{ web_protocol }}://{{ service_provider.contact.peertube.split('@')[2] }}/@{{ service_provider.contact.peertube.split('@')[1] }}" url: "{{ web_protocol }}://{{ service_provider.contact.peertube.split('@')[2] }}/@{{ service_provider.contact.peertube.split('@')[1] }}"
iframe: {{ applications | is_feature_enabled('landing_page_iframe','peertube') }} iframe: {{ applications | is_feature_enabled('portfolio_iframe','peertube') }}
{% endif %} {% endif %}
{% if service_provider.contact.wordpress is defined and service_provider.contact.wordpress != "" %} {% if service_provider.contact.wordpress is defined and service_provider.contact.wordpress != "" %}
@ -76,7 +76,7 @@ accounts:
class: fa-solid fa-blog class: fa-solid fa-blog
identifier: "{{service_provider.contact.wordpress}}" identifier: "{{service_provider.contact.wordpress}}"
url: "{{ web_protocol }}://{{ service_provider.contact.wordpress.split('@')[2] }}/@{{ service_provider.contact.wordpress.split('@')[1] }}" url: "{{ web_protocol }}://{{ service_provider.contact.wordpress.split('@')[2] }}/@{{ service_provider.contact.wordpress.split('@')[1] }}"
iframe: {{ applications | is_feature_enabled('landing_page_iframe','wordpress') }} iframe: {{ applications | is_feature_enabled('portfolio_iframe','wordpress') }}
{% endif %} {% endif %}
{% if service_provider.contact.source_code is defined and service_provider.contact.source_code != "" %} {% if service_provider.contact.source_code is defined and service_provider.contact.source_code != "" %}
@ -98,7 +98,7 @@ accounts:
class: fas fa-network-wired class: fas fa-network-wired
identifier: "{{service_provider.contact.friendica}}" identifier: "{{service_provider.contact.friendica}}"
url: "{{ web_protocol }}://{{ service_provider.contact.friendica.split('@')[2] }}/@{{ service_provider.contact.friendica.split('@')[1] }}" url: "{{ web_protocol }}://{{ service_provider.contact.friendica.split('@')[2] }}/@{{ service_provider.contact.friendica.split('@')[1] }}"
iframe: {{ applications | is_feature_enabled('landing_page_iframe','friendica') }} iframe: {{ applications | is_feature_enabled('portfolio_iframe','friendica') }}
{% endif %} {% endif %}

View File

@ -37,13 +37,13 @@
icon: icon:
class: fa-solid fa-shield-halved class: fa-solid fa-shield-halved
url: https://{{domains.keycloak}}/admin url: https://{{domains.keycloak}}/admin
iframe: {{ applications | is_feature_enabled('landing_page_iframe','keycloak') }} iframe: {{ applications | is_feature_enabled('portfolio_iframe','keycloak') }}
- name: Profile - name: Profile
description: Update your personal admin settings description: Update your personal admin settings
icon: icon:
class: fa-solid fa-user-gear class: fa-solid fa-user-gear
url: https://{{ domains.keycloak }}/realms/{{oidc.client.id}}/account url: https://{{ domains.keycloak }}/realms/{{oidc.client.id}}/account
iframe: {{ applications | is_feature_enabled('landing_page_iframe','keycloak') }} iframe: {{ applications | is_feature_enabled('portfolio_iframe','keycloak') }}
- name: Logout - name: Logout
description: End your admin session securely description: End your admin session securely
icon: icon:
@ -113,7 +113,7 @@
icon: icon:
class: fas fa-book class: fas fa-book
url: https://{{domains.sphinx}} url: https://{{domains.sphinx}}
iframe: {{ applications | is_feature_enabled('landing_page_iframe','sphinx') }} iframe: {{ applications | is_feature_enabled('portfolio_iframe','sphinx') }}
{% endif %} {% endif %}
@ -124,7 +124,7 @@
icon: icon:
class: "fas fa-chalkboard-teacher" class: "fas fa-chalkboard-teacher"
url: https://{{domains.presentation}} url: https://{{domains.presentation}}
iframe: {{ applications | is_feature_enabled('landing_page_iframe','presentation') }} iframe: {{ applications | is_feature_enabled('portfolio_iframe','presentation') }}
{% endif %} {% endif %}

View File

@ -1,7 +1,7 @@
features: features:
matomo: true matomo: true
css: true css: true
landingpage_iframe: false portfolio_iframe: false
csp: csp:
whitelist: whitelist:
script-src: script-src:

View File

@ -1,7 +1,7 @@
features: features:
matomo: true matomo: true
css: true css: true
landingpage_iframe: true portfolio_iframe: true
csp: csp:
whitelist: whitelist:

View File

@ -2,5 +2,5 @@ version: "latest"
features: features:
matomo: true matomo: true
css: true css: true
landingpage_iframe: false portfolio_iframe: false
central_database: true central_database: true

View File

@ -1,7 +1,7 @@
features: features:
matomo: true matomo: true
css: true css: true
landingpage_iframe: false portfolio_iframe: false
csp: csp:
flags: flags:
script-src: script-src:

View File

@ -9,7 +9,7 @@ flavor: 'taigaio' # Potential flavors: robrotheram, taigaio
features: features:
matomo: true matomo: true
css: true css: true
landingpage_iframe: false portfolio_iframe: false
oidc: false oidc: false
central_database: true central_database: true

View File

@ -13,7 +13,7 @@ plugins:
features: features:
matomo: true matomo: true
css: false css: false
landingpage_iframe: false portfolio_iframe: false
oidc: true oidc: true
central_database: true central_database: true
csp: csp:
@ -28,10 +28,9 @@ csp:
- "blob:" - "blob:"
font-src: font-src:
- "data:" - "data:"
- "https://fonts.bunny.net"
script-src: script-src:
- "https://cdn.gtranslate.net" - "https://cdn.gtranslate.net"
- "{{ domains.wordpress[0] }}" - "{{ domains.wordpress[0] }}"
frame-src:
- "{{ domains.peertube }}"
style-src: style-src:
- "https://fonts.bunny.net" - "https://fonts.bunny.net"

View File

@ -9,6 +9,6 @@ oauth2_proxy:
features: features:
matomo: true matomo: true
css: true css: true
landingpage_iframe: false portfolio_iframe: false
central_database: true central_database: true
oauth2: true oauth2: true

View File

@ -1,4 +1,4 @@
features: features:
matomo: true matomo: true
css: true css: true
landingpage_iframe: true portfolio_iframe: true

View File

@ -1,4 +1,4 @@
features: features:
matomo: true matomo: true
css: true css: true
landingpage_iframe: false portfolio_iframe: false

View File

@ -149,7 +149,7 @@ def update_mastodon():
Runs the database migration for Mastodon to ensure all required tables are up to date. Runs the database migration for Mastodon to ensure all required tables are up to date.
""" """
print("Starting Mastodon database migration.") print("Starting Mastodon database migration.")
run_command("docker compose exec -T web bash -c 'RAILS_ENV=production bin/rails db:migrate'") run_command("docker compose exec -T web bash -c 'RAILS_ENV={{ CYMAIS_ENVIRONMENT | lower }} bin/rails db:migrate'")
print("Mastodon database migration complete.") print("Mastodon database migration complete.")
def upgrade_listmonk(): def upgrade_listmonk():

1
tasks/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
include-docker-roles.yml

View File

@ -11,215 +11,8 @@
- health-btrfs - health-btrfs
- system-btrfs-auto-balancer - system-btrfs-auto-balancer
######################################################################### - name: "Integrate Docker Role includes"
### Docker Roles ### include_tasks: "include-docker-roles.yml"
#########################################################################
- name: "setup matomo"
when: ("matomo" in group_names)
include_role:
name: docker-matomo
- name: setup ldap
when: ("ldap" in group_names)
include_role:
name: docker-ldap
- name: setup keycloak
when: ("keycloak" in group_names)
include_role:
name: docker-keycloak
- name: setup lam
when: ("lam" in group_names)
include_role:
name: docker-lam
- name: setup phpldapadmin
when: ("phpldapadmin" in group_names)
include_role:
name: docker-phpldapadmin
- name: setup nextcloud hosts
when: ("nextcloud" in group_names)
include_role:
name: docker-nextcloud
- name: setup gitea hosts
when: ("gitea" in group_names)
include_role:
name: docker-gitea
vars:
run_mode: prod
- name: setup wordpress hosts
when: ("wordpress" in group_names)
include_role:
name: docker-wordpress
- name: setup mediawiki hosts
when: ("mediawiki" in group_names)
include_role:
name: docker-mediawiki
- name: setup mybb hosts
when: ("mybb" in group_names)
include_role:
name: docker-mybb
vars:
mybb_domains: "{{domains.mybb}}"
- name: setup yourls hosts
when: ("yourls" in group_names)
include_role:
name: docker-yourls
- name: setup mailu hosts
when: ("mailu" in group_names)
include_role:
name: docker-mailu
- name: setup elk hosts
when: ("elk" in group_names)
include_role:
name: docker-elk
- name: setup mastodon hosts
when: ("mastodon" in group_names)
include_role:
name: docker-mastodon
- name: setup pixelfed hosts
when: ("pixelfed" in group_names)
include_role:
name: docker-pixelfed
- name: setup peertube hosts
when: ("peertube" in group_names)
include_role:
name: docker-peertube
- name: setup bigbluebutton hosts
when: ("bigbluebutton" in group_names)
include_role:
name: docker-bigbluebutton
vars:
domain: "{{domains.bigbluebutton}}"
- name: setup funkwhale hosts
when: ("funkwhale" in group_names)
include_role:
name: docker-funkwhale
- name: setup roulette-wheel hosts
when: ("roulette-wheel" in group_names)
include_role:
name: docker-roulette-wheel
- name: setup joomla hosts
when: ("joomla" in group_names)
include_role:
name: docker-joomla
- name: setup attendize
when: ("attendize" in group_names)
include_role:
name: docker-attendize
- name: setup baserow hosts
when: ("baserow" in group_names)
include_role:
name: docker-baserow
- name: setup listmonk
when: ("listmonk" in group_names)
include_role:
name: docker-listmonk
- name: setup discourse
when: ("discourse" in group_names)
include_role:
name: docker-discourse
- name: setup matrix with flavor 'ansible'
include_role:
name: docker-matrix-ansible
when: applications.matrix.role == 'ansible' and ("matrix" in group_names)
- name: setup matrix with flavor 'compose'
include_role:
name: docker-matrix
when: applications.matrix.role == 'compose' and ("matrix" in group_names)
- name: setup open project instances
when: ("openproject" in group_names)
include_role:
name: docker-openproject
- name: setup gitlab hosts
when: ("gitlab" in group_names)
include_role:
name: docker-gitlab
- name: setup akaunting hosts
when: ("akaunting" in group_names)
include_role:
name: docker-akaunting
- name: setup moodle instance
when: ("moodle" in group_names)
include_role:
name: docker-moodle
- name: setup taiga instance
when: ("taiga" in group_names)
include_role:
name: docker-taiga
- name: setup friendica hosts
when: ("friendica" in group_names)
include_role:
name: docker-friendica
- name: setup portfolio
when: ("portfolio" in group_names)
include_role:
name: docker-portfolio
- name: setup bluesky
when: ("bluesky" in group_names)
include_role:
name: docker-bluesky
- name: setup PHPMyAdmin
when: ("phpmyadmin" in group_names)
include_role:
name: docker-phpmyadmin
- name: setup SNIPE-IT
when: ("snipe_it" in group_names)
include_role:
name: docker-snipe_it
- name: setup sphinx
when: ("sphinx" in group_names)
include_role:
name: docker-sphinx
- name: setup pgadmin
when: ("pgadmin" in group_names)
include_role:
name: docker-pgadmin
- name: setup presentation
when: ("presentation" in group_names)
include_role:
name: docker-presentation
- name: setup espocrm hosts
when: ("espocrm" in group_names)
include_role:
name: docker-espocrm
# Native Webserver Roles # Native Webserver Roles
- name: setup nginx-serve-htmls - name: setup nginx-serve-htmls

View File

@ -0,0 +1,64 @@
import os
import sys
import unittest
# Add the filter_plugins directory from the docker-matrix role to the import path
sys.path.insert(
0,
os.path.abspath(
os.path.join(os.path.dirname(__file__), "../../roles/docker-matrix")
),
)
from filter_plugins.bridge_filters import filter_enabled_bridges
class TestBridgeFilters(unittest.TestCase):
def test_no_bridges_returns_empty_list(self):
# When there are no bridges defined, result should be an empty list
self.assertEqual(filter_enabled_bridges([], {}), [])
def test_all_bridges_disabled(self):
# Define two bridges, but plugins dict has them disabled or missing
bridges = [
{'bridge_name': 'whatsapp', 'config': {}},
{'bridge_name': 'telegram', 'config': {}},
]
plugins = {
'whatsapp': False,
'telegram': False,
}
result = filter_enabled_bridges(bridges, plugins)
self.assertEqual(result, [])
def test_some_bridges_enabled(self):
# Only bridges with True in plugins should be returned
bridges = [
{'bridge_name': 'whatsapp', 'version': '1.0'},
{'bridge_name': 'telegram', 'version': '1.0'},
{'bridge_name': 'signal', 'version': '1.0'},
]
plugins = {
'whatsapp': True,
'telegram': False,
'signal': True,
}
result = filter_enabled_bridges(bridges, plugins)
expected = [
{'bridge_name': 'whatsapp', 'version': '1.0'},
{'bridge_name': 'signal', 'version': '1.0'},
]
self.assertEqual(result, expected)
def test_bridge_without_plugin_entry_defaults_to_disabled(self):
# If a bridge_name is not present in plugins, it should be treated as disabled
bridges = [
{'bridge_name': 'facebook', 'enabled': True},
]
plugins = {} # no entries
result = filter_enabled_bridges(bridges, plugins)
self.assertEqual(result, [])
if __name__ == "__main__":
unittest.main()

View File

@ -1,6 +1,16 @@
# tests/unit/test_configuration_filters.py # tests/unit/test_configuration_filters.py
import unittest import unittest
import sys
import os
sys.path.insert(
0,
os.path.abspath(
os.path.join(os.path.dirname(__file__), "../../")
),
)
from filter_plugins.configuration_filters import ( from filter_plugins.configuration_filters import (
is_feature_enabled, is_feature_enabled,
) )

View File

@ -1,6 +1,16 @@
import unittest import unittest
import hashlib import hashlib
import base64 import base64
import sys
import os
sys.path.insert(
0,
os.path.abspath(
os.path.join(os.path.dirname(__file__), "../../")
),
)
from filter_plugins.csp_filters import FilterModule, AnsibleFilterError from filter_plugins.csp_filters import FilterModule, AnsibleFilterError
class TestCspFilters(unittest.TestCase): class TestCspFilters(unittest.TestCase):
@ -137,5 +147,25 @@ class TestCspFilters(unittest.TestCase):
style_hash = self.filter.get_csp_hash("body { background: #fff; }") style_hash = self.filter.get_csp_hash("body { background: #fff; }")
self.assertNotIn(style_hash, header) self.assertNotIn(style_hash, header)
def test_build_csp_header_recaptcha_toggle(self):
"""
When the 'recaptcha' feature is enabled, 'https://www.google.com'
must be included in script-src; when disabled, it must not be.
"""
# enabled case
self.apps['app1']['features']['recaptcha'] = True
header_enabled = self.filter.build_csp_header(
self.apps, 'app1', self.domains, web_protocol='https'
)
self.assertIn("https://www.google.com", header_enabled)
# disabled case
self.apps['app1']['features']['recaptcha'] = False
header_disabled = self.filter.build_csp_header(
self.apps, 'app1', self.domains, web_protocol='https'
)
self.assertNotIn("https://www.google.com", header_disabled)
if __name__ == '__main__': if __name__ == '__main__':
unittest.main() unittest.main()

View File

@ -2,8 +2,12 @@ import os
import sys import sys
import unittest import unittest
PROJECT_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "../../..")) sys.path.insert(
sys.path.insert(0, PROJECT_ROOT) 0,
os.path.abspath(
os.path.join(os.path.dirname(__file__), "../../")
),
)
from filter_plugins.redirect_filters import FilterModule from filter_plugins.redirect_filters import FilterModule