Compare commits

...

7 Commits

66 changed files with 417 additions and 366 deletions

View File

@ -1,17 +1,26 @@
ROLES_DIR=./roles
OUTPUT=./group_vars/all/11_applications.yml
SCRIPT=./cli/generate_defaults_applications.py
ROLES_DIR := ./roles
APPLICATIONS_OUT := ./group_vars/all/11_applications.yml
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:
@echo "🔧 Generating $(OUTPUT) from roles in $(ROLES_DIR)..."
@mkdir -p $(dir $(OUTPUT))
python3 $(SCRIPT) --roles-dir $(ROLES_DIR) --output-file $(OUTPUT)
@echo "✅ Output written to $(OUTPUT)"
@echo "🔧 Generating applications defaults → $(APPLICATIONS_OUT) from roles in $(ROLES_DIR)"
@mkdir -p $(dir $(APPLICATIONS_OUT))
python3 $(APPLICATIONS_SCRIPT) --roles-dir $(ROLES_DIR) --output-file $(APPLICATIONS_OUT)
@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
@echo "⚙️ Install complete."
test:
@echo "Executing Unit Tests:"
@echo "🧪 Running Unit Tests..."
python -m unittest discover -s tests/unit
@echo "Executing Integration Tests:"
@echo "🔬 Running Integration Tests..."
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
matomo: true # Enable Matomo tracking for analytics
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
ldap: true # Enable ldap integration
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:
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
tokens += self.get_csp_whitelist(applications, application_id, directive)

View File

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

View File

@ -3,6 +3,7 @@
import argparse
import subprocess
import os
import datetime
def run_ansible_vault(action, filename, password_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)
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")
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")
subprocess.run(cmd, check=True)
end_time = datetime.datetime.now().isoformat()
print(f"\n✅ Script ended at: {end_time}\n")
def main():
# Change to script dir to execute all folders relative to their
script_dir = os.path.dirname(os.path.realpath(__file__))

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -12,7 +12,7 @@ SSH_PORT={{ports.public.ssh[application_id]}}
SSH_LISTEN_PORT=22
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]}}/"
# Mail Configuration

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -15,6 +15,6 @@ credentials:
features:
matomo: 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
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:
matomo: true
css: true
landingpage_iframe: false
portfolio_iframe: false
oidc: true
central_database: true

View File

@ -2,7 +2,7 @@ version: "latest"
features:
matomo: true
css: false
landingpage_iframe: false
portfolio_iframe: false
central_database: true
oauth2: false
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"
include_role:
name: docker-central-database

View File

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

View File

@ -45,7 +45,7 @@ email:
client_base_url: "{{domains.synapse}}"
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
oidc_providers:
- idp_id: keycloak
@ -61,7 +61,9 @@ oidc_providers:
backchannel_logout_enabled: true
{% endif %}
{% if bridges | bool %}
app_service_config_files:
{% for item in bridges %}
- {{registration_file_folder}}{{item.bridge_name}}.registration.yaml
{% 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:
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
role: "compose" # Role to setup Matrix. Valid values: ansible, compose
server_name: "{{primary_domain}}" # Adress for the account names etc.
synapse:
version: "latest"
@ -13,8 +12,8 @@ setup: false # Set true in inventory
features:
matomo: true
css: true
landingpage_iframe: false
oidc: false # Deactivated OIDC due to this issue https://github.com/matrix-org/synapse/issues/10492
portfolio_iframe: false
oidc: true # Deactivated OIDC due to this issue https://github.com/matrix-org/synapse/issues/10492
central_database: true
csp:
flags:
@ -30,3 +29,14 @@ csp:
script-src:
- "{{ domains.synapse }}"
- "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"
registration_file_folder: "/data/"
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"
features:
matomo: true
css: true
landingpage_iframe: false
css: false
portfolio_iframe: false
central_database: true
csp:
flags:
@ -20,4 +20,5 @@ csp:
font-src:
- "data:"
- "blob:"
script-src:
- "https://cdn.jsdelivr.net"

View File

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

View File

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

View File

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

View File

@ -96,7 +96,7 @@
shell: >
docker compose exec web bash -c "
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 }}');
if user.nil?;
puts 'User with email {{ users.administrator.email }} not found.';

View File

@ -49,7 +49,7 @@
- name: Set settings in OpenProject
shell: >
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:
chdir: "{{ docker_compose.directories.instance }}"
loop: "{{ openproject_rails_settings | dict2items }}"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,4 +1,4 @@
features:
matomo: 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.
"""
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.")
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
- system-btrfs-auto-balancer
#########################################################################
### Docker Roles ###
#########################################################################
- 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
- name: "Integrate Docker Role includes"
include_tasks: "include-docker-roles.yml"
# Native Webserver Roles
- 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
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 (
is_feature_enabled,
)

View File

@ -1,6 +1,16 @@
import unittest
import hashlib
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
class TestCspFilters(unittest.TestCase):
@ -137,5 +147,25 @@ class TestCspFilters(unittest.TestCase):
style_hash = self.filter.get_csp_hash("body { background: #fff; }")
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__':
unittest.main()

View File

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