20 Commits

Author SHA1 Message Date
3da645f3b8 Mailu/MSMTP: split token mgmt, idempotent reload, safer guards
• Rename: 02_create-user.yml → 02_manage_user.yml; 03_create-token.yml → 03a_manage_user_token.yml + 03b_create_user_token.yml
• Only (re)run sys-svc-msmtp when no-reply token exists; set run_once_sys_svc_msmtp=true in 01_core
• Reset by setting run_once_sys_svc_msmtp=false after creating no-reply token; then include sys-svc-msmtp
• Harden when-guards (no '{{ }}' in when, safe .get lookups)
• Minor formatting and failed_when readability

Conversation: https://chatgpt.com/share/68ebd196-a264-800f-a215-3a89d0f96c79
2025-10-12 18:05:00 +02:00
a996e2190f feat(logout): wire injector to web-svc-logout and add robust CORS/CSP for /logout
- sys-front-inj-logout: depend on web-svc-logout (run-once guarded) and simplify task flow.
- web-svc-logout: align feature flags/formatting and extend CSP:
  - add cdn.jsdelivr.net to connect/script/style and quote values.
- Nginx: move CORS config into logout-proxy.conf.j2 with dynamic vars:
  - Access-Control-Allow-Origin set to canonical logout origin,
  - Allow-Credentials=true,
  - Allow-Methods=GET, OPTIONS,
  - basic headers list (Accept, Authorization),
  - cache disabled for /logout responses.
- Drop obsolete CORS var passing from 01_core.yml; headers now templated at proxy layer.

Prepares clean cross-origin logout orchestration from https://logout.veen.world.

Refs: ChatGPT discussion – https://chatgpt.com/share/68ebb75f-0170-800f-93c5-e5cb438b8ed4
2025-10-12 16:16:47 +02:00
7dccffd52d Optimized variable whitespacing 2025-10-12 14:37:45 +02:00
853f2c3e2d Added mastodon to disc space cleanup script 2025-10-12 14:37:19 +02:00
b2978a3141 Changed mailu token name 2025-10-12 14:36:13 +02:00
0e0b703ccd Added cleanup to mastodon 2025-10-12 14:03:28 +02:00
0b86b2f057 Set MODE_CLEANUP default true and solved tld localhost user name bug 2025-10-12 01:15:52 +02:00
80e048a274 Fix: make EspoCRM entrypoint POSIX-compliant and remove illegal 'pipefail' usage
The previous entrypoint script used 'set -euo pipefail', which caused runtime errors
because /bin/sh (Dash/BusyBox) does not support 'pipefail'. This commit makes the
entrypoint fully POSIX-safe, adds robust fallbacks for missing scripts, and improves
logging. Also removes a trailing newline in the navigator Docker Compose template.

Related ChatGPT discussion: https://chatgpt.com/share/68eab0b7-7a64-800f-a8aa-e7d7262a262e
2025-10-11 21:33:07 +02:00
2610aec293 Deactivated cakeday plugin because it's an onboard plugin 2025-10-11 18:23:38 +02:00
07db162368 Reformated navigation role 2025-10-11 18:04:58 +02:00
a526d1adc4 Solved Keycloak Master Email Configuration Update settings 2025-10-11 16:57:36 +02:00
ca95079111 Added Email Configuration for Keycloak Master Realm 2025-10-11 16:45:50 +02:00
e410d66cb4 Add health check for Keycloak container and grant global 'admin' realm role to permanent admin user
This update waits for the Keycloak container to become healthy before attempting login and replaces the old realm-management based role assignment with the global 'admin' realm role.
See: https://chatgpt.com/share/68e99953-e988-800f-8b82-9ffb14c11910
2025-10-11 01:40:48 +02:00
ab48cf522f fix(keycloak): make permanent admin creation idempotent and fix password command
- prevent task failure when 'User exists with same username'
- remove invalid '--temporary false' flag from set-password command
- ensure realm-admin role grant remains idempotent

See: https://chatgpt.com/share/68e99271-fdb0-800f-a8ad-11c15d02a670
2025-10-11 01:10:57 +02:00
41c12bdc12 Refactor Keycloak permanent admin creation:
- Remove jq dependency to avoid container command errors
- Use username-based operations instead of user ID lookups
- Improve idempotency and portability across environments

See: https://chatgpt.com/share/68e98e77-9b3c-800f-8393-71b0be22cb46
2025-10-11 00:54:04 +02:00
aae463b602 Optimized keycloak setup procedures 2025-10-11 00:20:27 +02:00
bb50551533 Restrucutred keycloak accounts 2025-10-10 23:40:58 +02:00
098099b41e Refactored web-app-keycloak 2025-10-10 22:45:26 +02:00
0a7d767252 Added contains to filter 2025-10-10 22:16:23 +02:00
d88599f76c Added 'to_nice_json' exception 2025-10-10 22:16:22 +02:00
39 changed files with 380 additions and 221 deletions

View File

@@ -5,6 +5,6 @@ MODE_DUMMY: false # Executes dummy/test routines instead
MODE_UPDATE: true # Executes updates
MODE_DEBUG: false # This enables debugging in ansible and in the apps, You SHOULD NOT enable this on production servers
MODE_RESET: false # Cleans up all Infinito.Nexus files. It's necessary to run to whole playbook and not particial roles when using this function.
MODE_CLEANUP: "{{ MODE_DEBUG | bool }}" # Cleanup unused files and configurations
MODE_CLEANUP: true # Cleanup unused files and configurations
MODE_ASSERT: "{{ MODE_DEBUG | bool }}" # Executes validation tasks during the run.
MODE_BACKUP: true # Executes the Backup before the deployment

View File

@@ -1,8 +1,7 @@
- name: Include dependencies
include_role:
name: '{{ item }}'
loop:
- sys-svc-msmtp
name: "sys-svc-msmtp"
when: run_once_sys_svc_msmtp is not defined or run_once_sys_svc_msmtp is false
- include_role:
name: sys-service

View File

@@ -17,7 +17,7 @@
name: sys-service
vars:
system_service_tpl_on_failure: "{{ SYS_SERVICE_ON_FAILURE_COMPOSE }}"
system_service_tpl_exec_start: "{{ system_service_script_exec }} --backups-folder-path {{ BACKUPS_FOLDER_PATH }} --maximum-backup-size-percent {{SIZE_PERCENT_MAXIMUM_BACKUP}}"
system_service_tpl_exec_start: "{{ system_service_script_exec }} --backups-folder-path {{ BACKUPS_FOLDER_PATH }} --maximum-backup-size-percent {{ SIZE_PERCENT_MAXIMUM_BACKUP }}"
system_service_tpl_exec_start_pre: '/usr/bin/python {{ PATH_SYSTEM_LOCK_SCRIPT }} {{ SYS_SERVICE_GROUP_MANIPULATION | join(" ") }} --ignore {{ SYS_SERVICE_GROUP_CLEANUP | join(" ") }} --timeout "{{ SYS_TIMEOUT_BACKUP_SERVICES }}"'
system_service_copy_files: true
system_service_force_linear_sync: false

View File

@@ -39,6 +39,18 @@ if [ "$force_freeing" = true ]; then
docker exec -u www-data $nextcloud_application_container /var/www/html/occ versions:cleanup || exit 6
fi
# Mastodon cleanup (remote media cache)
mastodon_application_container="{{ applications | get_app_conf('web-app-mastodon', 'docker.services.mastodon.name') }}"
mastodon_cleanup_days="1"
if [ -n "$mastodon_application_container" ] && docker ps -a --format '{% raw %}{{.Names}}{% endraw %}' | grep -qw "$mastodon_application_container"; then
echo "Cleaning up Mastodon media cache (older than ${mastodon_cleanup_days} days)" &&
docker exec -u root "$mastodon_application_container" bash -lc "bin/tootctl media remove --days=${mastodon_cleanup_days}" || exit 8
# Optional: additionally remove local thumbnail/cache files older than X days
# Warning: these will be regenerated when accessed, which may cause extra CPU/I/O load
# docker exec -u root "$mastodon_application_container" bash -lc "find /mastodon/public/system/cache -type f -mtime +${mastodon_cleanup_days} -delete" || exit 9
fi
fi
if command -v pacman >/dev/null 2>&1 ; then

View File

@@ -1,8 +1,12 @@
- name: Include dependency 'sys-svc-webserver-core'
- name: Include dependency 'web-svc-logout'
include_role:
name: sys-svc-webserver-core
name: web-svc-logout
when:
- run_once_sys_svc_webserver_core is not defined
- run_once_web_svc_logout is not defined
- name: "deploy the logout.js"
include_tasks: "02_deploy.yml"
include_tasks: "02_deploy.yml"
- set_fact:
run_once_sys_front_inj_logout: true
changed_when: false

View File

@@ -1,7 +1,5 @@
- block:
- include_tasks: 01_core.yml
- set_fact:
run_once_sys_front_inj_logout: true
- name: "Load base for '{{ application_id }}'"
include_tasks: 01_core.yml
when: run_once_sys_front_inj_logout is not defined
- name: "Load logout code for '{{ application_id }}'"

View File

@@ -14,4 +14,7 @@
- include_role:
name: sys-ctl-hlth-msmtp
when: run_once_sys_ctl_hlth_msmtp is not defined
when: run_once_sys_ctl_hlth_msmtp is not defined
- set_fact:
run_once_sys_svc_msmtp: true

View File

@@ -1,5 +1,6 @@
- block:
- include_tasks: 01_core.yml
- set_fact:
run_once_sys_svc_msmtp: true
when: run_once_sys_svc_msmtp is not defined
- name: "Load MSMTP Core Once"
include_tasks: 01_core.yml
when:
- run_once_sys_svc_msmtp is not defined or run_once_sys_svc_msmtp is false
# Just execute when mailu_token is defined
- users['no-reply'].mailu_token is defined

View File

@@ -5,7 +5,7 @@ users:
username: "{{ PRIMARY_DOMAIN.split('.')[0] }}"
tld:
description: "Auto Generated Account to reserve the TLD"
username: "{{ PRIMARY_DOMAIN.split('.')[1] }}"
username: "{{ PRIMARY_DOMAIN.split('.')[1] if (PRIMARY_DOMAIN is defined and (PRIMARY_DOMAIN.split('.') | length) > 1) else (PRIMARY_DOMAIN ~ '_tld ') }}"
root:
username: root
uid: 0

View File

@@ -43,9 +43,10 @@ plugins:
enabled: true
discourse-akismet:
enabled: true
discourse-cakeday:
enabled: true
# discourse-solved: Seems like this plugin is now also part of the default setup
# The following plugins moved to the default setup
# discourse-cakeday:
# enabled: true
# discourse-solved:
# enabled: true
# discourse-voting:
# enabled: true

View File

@@ -1,11 +1,13 @@
#!/bin/sh
set -euo pipefail
# POSIX-safe entrypoint for EspoCRM container
# Compatible with /bin/sh (dash/busybox). Avoids 'pipefail' and non-portable features.
set -eu
log() { printf '%s %s\n' "[entrypoint]" "$*" >&2; }
# --- Simple boolean normalization --------------------------------------------
bool_norm () {
v="$(printf '%s' "${1:-}" | tr '[:upper:]' '[:lower:]')"
v="$(printf '%s' "${1:-}" | tr '[:upper:]' '[:lower:]' 2>/dev/null || true)"
case "$v" in
1|true|yes|on) echo "true" ;;
0|false|no|off|"") echo "false" ;;
@@ -13,30 +15,45 @@ bool_norm () {
esac
}
# Expected ENV (from env.j2)
# --- Environment initialization ----------------------------------------------
MAINTENANCE="$(bool_norm "${ESPO_INIT_MAINTENANCE_MODE:-false}")"
CRON_DISABLED="$(bool_norm "${ESPO_INIT_CRON_DISABLED:-false}")"
USE_CACHE="$(bool_norm "${ESPO_INIT_USE_CACHE:-true}")"
APP_DIR="/var/www/html"
SET_FLAGS_SCRIPT="${ESPOCRM_SET_FLAGS_SCRIPT}"
# Provided by env.j2 (fallback ensures robustness)
SET_FLAGS_SCRIPT="${ESPOCRM_SET_FLAGS_SCRIPT:-/usr/local/bin/set_flags.php}"
if [ ! -f "$SET_FLAGS_SCRIPT" ]; then
log "WARN: SET_FLAGS_SCRIPT '$SET_FLAGS_SCRIPT' not found; falling back to /usr/local/bin/set_flags.php"
SET_FLAGS_SCRIPT="/usr/local/bin/set_flags.php"
fi
# --- Wait for bootstrap.php (max 60s, e.g. fresh volume) ----------------------
log "Waiting for ${APP_DIR}/bootstrap.php..."
for i in $(seq 1 60); do
[ -f "${APP_DIR}/bootstrap.php" ] && break
count=0
while [ $count -lt 60 ] && [ ! -f "${APP_DIR}/bootstrap.php" ]; do
sleep 1
count=$((count + 1))
done
if [ ! -f "${APP_DIR}/bootstrap.php" ]; then
log "ERROR: bootstrap.php missing after 60s"; exit 1
log "ERROR: bootstrap.php missing after 60s"
exit 1
fi
# --- Apply config flags via set_flags.php ------------------------------------
log "Applying runtime flags via set_flags.php..."
php "${SET_FLAGS_SCRIPT}"
if ! php "${SET_FLAGS_SCRIPT}"; then
log "ERROR: set_flags.php execution failed"
exit 1
fi
# --- Clear cache (safe) -------------------------------------------------------
php "${APP_DIR}/clear_cache.php" || true
if php "${APP_DIR}/clear_cache.php" 2>/dev/null; then
log "Cache cleared successfully."
else
log "WARN: Cache clearing skipped or failed (non-critical)."
fi
# --- Hand off to CMD ----------------------------------------------------------
if [ "$#" -gt 0 ]; then
@@ -56,5 +73,6 @@ for cmd in apache2-foreground httpd-foreground php-fpm php-fpm8.3 php-fpm8.2 sup
fi
done
# --- Fallback ---------------------------------------------------------------
log "No known server command found; tailing to keep container alive."
exec tail -f /dev/null

View File

@@ -1,6 +1,6 @@
load_dependencies: True # When set to false the dependencies aren't loaded. Helpful for developing
actions:
import_realm: True # Import REALM
import_realm: True # Import REALM
features:
matomo: true
css: true
@@ -49,4 +49,10 @@ docker:
credentials:
recaptcha:
website_key: "" # Required if you enabled recaptcha:
secret_key: "" # Required if you enabled recaptcha:
secret_key: "" # Required if you enabled recaptcha:
accounts:
bootstrap:
username: "administrator"
system:
username: "{{ SOFTWARE_NAME | replace('.', '_') | lower }}"

View File

@@ -0,0 +1,89 @@
- name: "Wait until '{{ KEYCLOAK_CONTAINER }}' container is healthy"
community.docker.docker_container_info:
name: "{{ KEYCLOAK_CONTAINER }}"
register: kc_info
retries: 60
delay: 5
until: >
kc_info is succeeded and
(kc_info.container | default({})) != {} and
(kc_info.container.State | default({})) != {} and
(kc_info.container.State.Health | default({})) != {} and
(kc_info.container.State.Health.Status | default('')) == 'healthy'
- name: Ensure permanent Keycloak admin exists and can log in (container env only)
block:
- name: Try login with permanent admin (uses container ENV)
shell: |
{{ KEYCLOAK_EXEC_CONTAINER }} sh -lc '
{{ KEYCLOAK_KCADM }} config credentials \
--server {{ KEYCLOAK_SERVER_INTERNAL_URL }} \
--realm master \
--user "$KEYCLOAK_PERMANENT_ADMIN_USERNAME" \
--password "$KEYCLOAK_PERMANENT_ADMIN_PASSWORD"
'
register: kc_login_perm
changed_when: false
rescue:
- name: Login with bootstrap admin (uses container ENV)
shell: |
{{ KEYCLOAK_EXEC_CONTAINER }} sh -lc '
{{ KEYCLOAK_KCADM }} config credentials \
--server {{ KEYCLOAK_SERVER_INTERNAL_URL }} \
--realm master \
--user "$KC_BOOTSTRAP_ADMIN_USERNAME" \
--password "$KC_BOOTSTRAP_ADMIN_PASSWORD"
'
register: kc_login_bootstrap
changed_when: false
- name: Ensure permanent admin user exists (create if missing)
shell: |
{{ KEYCLOAK_EXEC_CONTAINER }} sh -lc '
{{ KEYCLOAK_KCADM }} create users -r master \
-s "username=$KEYCLOAK_PERMANENT_ADMIN_USERNAME" \
-s "enabled=true"
'
register: kc_create_perm_admin
failed_when: >
not (
kc_create_perm_admin.rc == 0 or
(kc_create_perm_admin.stderr is defined and
('User exists with same username' in kc_create_perm_admin.stderr))
)
changed_when: kc_create_perm_admin.rc == 0
- name: Set permanent admin password (by username, no ID needed)
shell: |
{{ KEYCLOAK_EXEC_CONTAINER }} sh -lc '
{{ KEYCLOAK_KCADM }} set-password -r master \
--username "$KEYCLOAK_PERMANENT_ADMIN_USERNAME" \
--new-password "$KEYCLOAK_PERMANENT_ADMIN_PASSWORD"
'
changed_when: true
- name: Grant global admin via master realm role 'admin'
shell: |
{{ KEYCLOAK_EXEC_CONTAINER }} sh -lc '
{{ KEYCLOAK_KCADM }} add-roles -r master \
--uusername "$KEYCLOAK_PERMANENT_ADMIN_USERNAME" \
--rolename admin
'
register: kc_grant_master_admin
changed_when: (kc_grant_master_admin.stderr is defined and kc_grant_master_admin.stderr | length > 0) or
(kc_grant_master_admin.stdout is defined and kc_grant_master_admin.stdout | length > 0)
failed_when: false
- name: Verify login with permanent admin (after creation)
shell: |
{{ KEYCLOAK_EXEC_CONTAINER }} sh -lc '
{{ KEYCLOAK_KCADM }} config credentials \
--server {{ KEYCLOAK_SERVER_INTERNAL_URL }} \
--realm master \
--user "$KEYCLOAK_PERMANENT_ADMIN_USERNAME" \
--password "$KEYCLOAK_PERMANENT_ADMIN_PASSWORD"
'
changed_when: false

View File

@@ -13,82 +13,21 @@
include_tasks: 04_dependencies.yml
when: KEYCLOAK_LOAD_DEPENDENCIES | bool
- name: "Wait until '{{ KEYCLOAK_CONTAINER }}' container is healthy"
community.docker.docker_container_info:
name: "{{ KEYCLOAK_CONTAINER }}"
register: kc_info
retries: 60
delay: 5
until: >
kc_info is succeeded and
(kc_info.container | default({})) != {} and
(kc_info.container.State | default({})) != {} and
(kc_info.container.State.Health | default({})) != {} and
(kc_info.container.State.Health.Status | default('')) == 'healthy'
- name: "Load Login routines for '{{ application_id }}'"
include_tasks: 05_login.yml
- name: kcadm login (master)
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
shell: >
{{ KEYCLOAK_EXEC_KCADM }} config credentials
--server {{ KEYCLOAK_SERVER_INTERNAL_URL }}
--realm master
--user {{ KEYCLOAK_MASTER_API_USER_NAME }}
--password {{ KEYCLOAK_MASTER_API_USER_PASSWORD }}
changed_when: false
- name: "Load Client Update routines for '{{ application_id }}'"
include_tasks: update/01_client.yml
- name: "Update Client settings"
vars:
kc_object_kind: "client"
kc_lookup_value: "{{ KEYCLOAK_CLIENT_ID }}"
kc_desired: >-
{{
KEYCLOAK_DICTIONARY_REALM.clients
| selectattr('clientId','equalto', KEYCLOAK_CLIENT_ID)
| list | first
}}
kc_force_attrs:
publicClient: >-
{{
(KEYCLOAK_DICTIONARY_REALM.clients
| selectattr('clientId','equalto', KEYCLOAK_CLIENT_ID)
| map(attribute='publicClient')
| first)
}}
serviceAccountsEnabled: >-
{{
(KEYCLOAK_DICTIONARY_REALM.clients
| selectattr('clientId','equalto', KEYCLOAK_CLIENT_ID)
| map(attribute='serviceAccountsEnabled')
| first )
}}
frontchannelLogout: >-
{{
(KEYCLOAK_DICTIONARY_REALM.clients
| selectattr('clientId','equalto', KEYCLOAK_CLIENT_ID)
| map(attribute='frontchannelLogout')
| first)
}}
attributes: >-
{{
( (KEYCLOAK_DICTIONARY_REALM.clients
| selectattr('clientId','equalto', KEYCLOAK_CLIENT_ID)
| list | first | default({}) ).attributes | default({}) )
| combine({'frontchannel.logout.url': KEYCLOAK_FRONTCHANNEL_LOGOUT_URL}, recursive=True)
}}
include_tasks: _update.yml
- name: "Load Mail Update routines for '{{ application_id }} - {{ KEYCLOAK_REALM }}'"
include_tasks: update/02_mail_realm.yml
- name: "Update REALM mail settings from realm dictionary (SPOT)"
include_tasks: _update.yml
vars:
kc_object_kind: "realm"
kc_lookup_field: "id"
kc_lookup_value: "{{ KEYCLOAK_REALM }}"
kc_desired:
smtpServer: "{{ KEYCLOAK_DICTIONARY_REALM.smtpServer | default({}, true) }}"
kc_merge_path: "smtpServer"
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
- name: "Load Mail Update routines for '{{ application_id }} - master'"
include_tasks: update/03_mail_master.yml
- include_tasks: 05_rbac_client_scope.yml
- name: "Load RBAC Update routines for '{{ application_id }}'"
include_tasks: update/04_rbac_client_scope.yml
- include_tasks: 06_ldap.yml
- name: "Load LDAP Update routines for '{{ application_id }}'"
include_tasks: update/05_ldap.yml
when: KEYCLOAK_LDAP_ENABLED | bool

View File

@@ -0,0 +1,40 @@
- name: "Update Client settings"
vars:
kc_object_kind: "client"
kc_lookup_value: "{{ KEYCLOAK_CLIENT_ID }}"
kc_desired: >-
{{
KEYCLOAK_DICTIONARY_REALM.clients
| selectattr('clientId','equalto', KEYCLOAK_CLIENT_ID)
| list | first
}}
kc_force_attrs:
publicClient: >-
{{
(KEYCLOAK_DICTIONARY_REALM.clients
| selectattr('clientId','equalto', KEYCLOAK_CLIENT_ID)
| map(attribute='publicClient')
| first)
}}
serviceAccountsEnabled: >-
{{
(KEYCLOAK_DICTIONARY_REALM.clients
| selectattr('clientId','equalto', KEYCLOAK_CLIENT_ID)
| map(attribute='serviceAccountsEnabled')
| first )
}}
frontchannelLogout: >-
{{
(KEYCLOAK_DICTIONARY_REALM.clients
| selectattr('clientId','equalto', KEYCLOAK_CLIENT_ID)
| map(attribute='frontchannelLogout')
| first)
}}
attributes: >-
{{
( (KEYCLOAK_DICTIONARY_REALM.clients
| selectattr('clientId','equalto', KEYCLOAK_CLIENT_ID)
| list | first | default({}) ).attributes | default({}) )
| combine({'frontchannel.logout.url': KEYCLOAK_FRONTCHANNEL_LOGOUT_URL}, recursive=True)
}}
include_tasks: _update.yml

View File

@@ -0,0 +1,10 @@
- name: "Update {{ KEYCLOAK_REALM }} REALM mail settings from realm dictionary"
include_tasks: _update.yml
vars:
kc_object_kind: "realm"
kc_lookup_field: "id"
kc_lookup_value: "{{ KEYCLOAK_REALM }}"
kc_desired:
smtpServer: "{{ KEYCLOAK_DICTIONARY_REALM.smtpServer | default({}, true) }}"
kc_merge_path: "smtpServer"
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"

View File

@@ -0,0 +1,10 @@
- name: "Update Master REALM mail settings from realm dictionary"
include_tasks: _update.yml
vars:
kc_object_kind: "realm"
kc_lookup_field: "id"
kc_lookup_value: "master"
kc_desired:
smtpServer: "{{ KEYCLOAK_DICTIONARY_REALM.smtpServer | default({}, true) }}"
kc_merge_path: "smtpServer"
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"

View File

@@ -1,4 +1,3 @@
# --- Ensure RBAC client scope exists (idempotent) ---
- name: Ensure RBAC client scope exists
shell: |
cat <<'JSON' | {{ KEYCLOAK_EXEC_KCADM }} create client-scopes -r {{ KEYCLOAK_REALM }} -f -
@@ -16,7 +15,6 @@
('already exists' not in (create_rbac_scope.stderr | lower))
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
# --- Get the scope id we will attach to the client ---
- name: Get all client scopes
shell: "{{ KEYCLOAK_EXEC_KCADM }} get client-scopes -r {{ KEYCLOAK_REALM }} --format json"
register: all_scopes

View File

@@ -10,19 +10,21 @@ KC_HTTP_ENABLED= true
KC_HEALTH_ENABLED= {{ KEYCLOAK_HEALTH_ENABLED | lower }}
KC_METRICS_ENABLED= true
# Administrator
KEYCLOAK_ADMIN= "{{ KEYCLOAK_ADMIN }}"
KEYCLOAK_ADMIN_PASSWORD= "{{ KEYCLOAK_ADMIN_PASSWORD }}"
# Database
KC_DB= {{ database_type }}
KC_DB_URL= {{ database_url_jdbc }}
KC_DB_USERNAME= {{ database_username }}
KC_DB_PASSWORD= {{ database_password }}
# If the initial administrator already exists and the environment variables are still present at startup, an error message stating the failed creation of the initial administrator is shown in the logs. Keycloak ignores the values and starts up correctly.
KC_BOOTSTRAP_ADMIN_USERNAME= "{{ KEYCLOAK_ADMIN }}"
KC_BOOTSTRAP_ADMIN_PASSWORD= "{{ KEYCLOAK_ADMIN_PASSWORD }}"
# Credentials
## Bootstrap
KC_BOOTSTRAP_ADMIN_USERNAME="{{ KEYCLOAK_BOOTSTRAP_ADMIN_USERNAME }}"
KC_BOOTSTRAP_ADMIN_PASSWORD="{{ KEYCLOAK_BOOTSTRAP_ADMIN_PASSWORD }}"
## Permanent
KEYCLOAK_PERMANENT_ADMIN_USERNAME="{{ KEYCLOAK_PERMANENT_ADMIN_USERNAME }}"
KEYCLOAK_PERMANENT_ADMIN_PASSWORD="{{ KEYCLOAK_PERMANENT_ADMIN_PASSWORD }}"
# Enable detailed logs
{% if MODE_DEBUG | bool %}

View File

@@ -1,3 +0,0 @@
users:
administrator:
username: "administrator"

View File

@@ -29,14 +29,22 @@ KEYCLOAK_REALM_IMPORT_FILE_SRC: "import/realm.json.j2"
KEYCLOAK_REALM_IMPORT_FILE_DST: "{{ [KEYCLOAK_REALM_IMPORT_DIR_HOST,'realm.json'] | path_join }}"
## Credentials
KEYCLOAK_ADMIN: "{{ applications | get_app_conf(application_id, 'users.administrator.username') }}"
KEYCLOAK_ADMIN_PASSWORD: "{{ applications | get_app_conf(application_id, 'credentials.administrator_password') }}"
### Bootstrap
KEYCLOAK_BOOTSTRAP_ADMIN_USERNAME: "{{ applications | get_app_conf(application_id, 'accounts.bootstrap.username') }}"
KEYCLOAK_BOOTSTRAP_ADMIN_PASSWORD: "{{ applications | get_app_conf(application_id, 'credentials.administrator_password') }}"
### Permanent
KEYCLOAK_PERMANENT_ADMIN_USERNAME: "{{ applications | get_app_conf(application_id, 'accounts.system.username') }}"
KEYCLOAK_PERMANENT_ADMIN_PASSWORD: "{{ applications | get_app_conf(application_id, 'credentials.administrator_password') }}"
## Docker
KEYCLOAK_CONTAINER: "{{ applications | get_app_conf(application_id, 'docker.services.keycloak.name') }}" # Name of the keycloak docker container
KEYCLOAK_EXEC_KCADM: "docker exec -i {{ KEYCLOAK_CONTAINER }} /opt/keycloak/bin/kcadm.sh" # Init script for keycloak
KEYCLOAK_IMAGE: "{{ applications | get_app_conf(application_id, 'docker.services.keycloak.image') }}" # Keycloak docker image
KEYCLOAK_VERSION: "{{ applications | get_app_conf(application_id, 'docker.services.keycloak.version') }}" # Keycloak docker version
KEYCLOAK_CONTAINER: "{{ applications | get_app_conf(application_id, 'docker.services.keycloak.name') }}"
KEYCLOAK_EXEC_CONTAINER: "docker exec -i {{ KEYCLOAK_CONTAINER }}"
KEYCLOAK_KCADM: "/opt/keycloak/bin/kcadm.sh"
KEYCLOAK_EXEC_KCADM: "{{ KEYCLOAK_EXEC_CONTAINER }} {{ KEYCLOAK_KCADM }}"
KEYCLOAK_IMAGE: "{{ applications | get_app_conf(application_id, 'docker.services.keycloak.image') }}"
KEYCLOAK_VERSION: "{{ applications | get_app_conf(application_id, 'docker.services.keycloak.version') }}"
## Server
KEYCLOAK_SERVER_HOST: "127.0.0.1:{{ ports.localhost.http[application_id] }}"
@@ -69,11 +77,6 @@ KEYCLOAK_LDAP_USER_OBJECT_CLASSES: >
) | join(', ')
}}
## API
KEYCLOAK_MASTER_API_USER: "{{ applications | get_app_conf(application_id, 'users.administrator') }}" # Master Administrator
KEYCLOAK_MASTER_API_USER_NAME: "{{ KEYCLOAK_MASTER_API_USER.username }}" # Master Administrator Username
KEYCLOAK_MASTER_API_USER_PASSWORD: "{{ KEYCLOAK_MASTER_API_USER.password }}" # Master Administrator Password
# Dictionaries
KEYCLOAK_DICTIONARY_REALM_RAW: "{{ lookup('template', 'import/realm.json.j2') }}"
KEYCLOAK_DICTIONARY_REALM: >-

View File

@@ -41,7 +41,7 @@
meta: flush_handlers
- name: "Create Mailu accounts"
include_tasks: 02_create-user.yml
include_tasks: 02_manage_user.yml
vars:
MAILU_DOCKER_DIR: "{{ docker_compose.directories.instance }}"
mailu_api_base_url: "http://127.0.0.1:8080/api/v1"

View File

@@ -25,5 +25,5 @@
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
- name: "Create Mailu API Token for {{ mailu_user_name }}"
include_tasks: 03_create-token.yml
when: "{{ 'mail-bot' in item.value.roles }}"
include_tasks: 03a_manage_user_token.yml
when: "'mail-bot' in item.value.roles"

View File

@@ -0,0 +1,26 @@
- name: "Fetch existing API tokens via curl inside admin container"
command: >-
{{ docker_compose_command_exec }} -T admin \
curl -s -X GET {{ mailu_api_base_url }}/token \
-H "Authorization: Bearer {{ MAILU_API_TOKEN }}"
args:
chdir: "{{ MAILU_DOCKER_DIR }}"
register: mailu_tokens_cli
changed_when: false
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
- name: "Extract existing token info for '{{ mailu_user_key }};{{ mailu_user_name }}'"
set_fact:
mailu_user_existing_token: >-
{{ (
mailu_tokens_cli.stdout
| default('[]')
| from_json
| selectattr('comment','equalto', SOFTWARE_NAME)
| list
).0 | default(None) }}
- name: "Start Mailu token procedures for undefined tokens"
when: users[mailu_user_key].mailu_token is not defined
include_tasks: 03b_create_user_token.yml

View File

@@ -1,26 +1,3 @@
- name: "Fetch existing API tokens via curl inside admin container"
command: >-
{{ docker_compose_command_exec }} -T admin \
curl -s -X GET {{ mailu_api_base_url }}/token \
-H "Authorization: Bearer {{ MAILU_API_TOKEN }}"
args:
chdir: "{{ MAILU_DOCKER_DIR }}"
register: mailu_tokens_cli
changed_when: false
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
- name: "Extract existing token info for '{{ mailu_user_key }};{{ mailu_user_name }}'"
set_fact:
mailu_user_existing_token: >-
{{ (
mailu_tokens_cli.stdout
| default('[]')
| from_json
| selectattr('comment','equalto', mailu_user_key ~ " - ansible.infinito")
| list
).0 | default(None) }}
- name: "Delete existing API token for '{{ mailu_user_key }};{{ mailu_user_name }}' if local token missing but remote exists"
command: >-
{{ docker_compose_command_exec }} -T admin \
@@ -29,7 +6,6 @@
args:
chdir: "{{ MAILU_DOCKER_DIR }}"
when:
- users[mailu_user_key].mailu_token is not defined
- mailu_user_existing_token is not none
- mailu_user_existing_token.id is defined
register: mailu_token_delete
@@ -43,13 +19,12 @@
-H "Authorization: Bearer {{ MAILU_API_TOKEN }}"
-H "Content-Type: application/json"
-d '{{ {
"comment": mailu_user_key ~ " - ansible.infinito",
"comment": SOFTWARE_NAME,
"email": users[mailu_user_key].email,
"ip": mailu_token_ip
} | to_json }}'
args:
chdir: "{{ MAILU_DOCKER_DIR }}"
when: users[mailu_user_key].mailu_token is not defined
register: mailu_token_creation
# If curl sees 4xx/5xx it returns non-zero due to -f → fail the task.
failed_when:
@@ -57,7 +32,7 @@
# Fallback: if some gateway returns 200 but embeds an error JSON.
- mailu_token_creation.rc == 0 and
(mailu_token_creation.stdout is search('"code"\\s*:\\s*4\\d\\d') or
mailu_token_creation.stdout is search('cannot be found'))
mailu_token_creation.stdout is search('cannot be found'))
# Only mark changed when a token is actually present in the JSON.
changed_when: mailu_token_creation.stdout is search('"token"\\s*:')
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
@@ -66,14 +41,25 @@
set_fact:
users: >-
{{ users
| combine({
mailu_user_key: (
users[mailu_user_key]
| combine({
'mailu_token': (mailu_token_creation.stdout | from_json).token
})
)
}, recursive=True)
| combine({
mailu_user_key: (
users[mailu_user_key]
| combine({
'mailu_token': (mailu_token_creation.stdout | from_json).token
})
)
}, recursive=True)
}}
when: users[mailu_user_key].mailu_token is not defined
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
- name: "Reset MSMTP Configuration if No-Reply User Token changed"
when: users['no-reply'].username == mailu_user_name
block:
- name: "Set MSMTP run-once fact false"
set_fact:
run_once_sys_svc_msmtp: false
changed_when: false
- name: Reload MSMTP role
include_role:
name: "sys-svc-msmtp"

View File

@@ -0,0 +1,19 @@
- name: Check health status of '{{ item }}' container
shell: |
cid=$(docker compose ps -q {{ item }})
docker inspect \
--format '{{ "{{.State.Health.Status}}" }}' \
$cid
args:
chdir: "{{ docker_compose.directories.instance }}"
register: healthcheck
retries: 60
delay: 5
until: healthcheck.stdout == "healthy"
loop:
- mastodon
- streaming
- sidekiq
loop_control:
label: "{{ item }}"
changed_when: false

View File

@@ -0,0 +1,9 @@
---
# Cleanup routine for Mastodon
# Removes cached remote media older than 14 days when MODE_CLEANUP is enabled.
- name: "Cleanup Mastodon media cache older than 14 days"
command:
cmd: "docker exec -u root {{ MASTODON_CONTAINER }} bin/tootctl media remove --days=14"
register: mastodon_cleanup
changed_when: mastodon_cleanup.rc == 0
failed_when: mastodon_cleanup.rc != 0

View File

@@ -1,6 +1,3 @@
- name: "Execute migration for '{{ application_id }}'"
command:
cmd: "docker exec {{ MASTODON_CONTAINER }} bundle exec rails db:migrate"
- name: "Include administrator routines for '{{ application_id }}'"
include_tasks: 02_administrator.yml

View File

@@ -1,26 +1,5 @@
# Routines to create the administrator account
# @see https://chatgpt.com/share/67b9b12c-064c-800f-9354-8e42e6459764
- name: Check health status of '{{ item }}' container
shell: |
cid=$(docker compose ps -q {{ item }})
docker inspect \
--format '{{ "{{.State.Health.Status}}" }}' \
$cid
args:
chdir: "{{ docker_compose.directories.instance }}"
register: healthcheck
retries: 60
delay: 5
until: healthcheck.stdout == "healthy"
loop:
- mastodon
- streaming
- sidekiq
loop_control:
label: "{{ item }}"
changed_when: false
- name: Remove line containing "- administrator" from config/settings.yml to allow creating administrator account
command:
cmd: "docker exec -u root {{ MASTODON_CONTAINER }} sed -i '/- administrator/d' config/settings.yml"

View File

@@ -18,5 +18,15 @@
vars:
docker_compose_flush_handlers: true
- name: "Wait for Mastodon"
include_tasks: 01_wait.yml
- name: "Cleanup Mastodon caches when MODE_CLEANUP is true"
include_tasks: 02_cleanup.yml
when: MODE_CLEANUP | bool
- name: "start setup procedures for mastodon"
include_tasks: 01_setup.yml
include_tasks: 03_setup.yml
- name: "Include administrator routines for '{{ application_id }}'"
include_tasks: 04_administrator.yml

View File

@@ -1,8 +1,8 @@
features:
matomo: true
css: true
matomo: true
css: true
desktop: true
logout: false
logout: false
server:
csp:
whitelist:
@@ -16,14 +16,15 @@ server:
font-src:
- https://cdnjs.cloudflare.com
frame-src:
- "{{ WEB_PROTOCOL }}://*.{{ PRIMARY_DOMAIN }}" # Makes sense that all of the website content is available in the navigator
# Makes sense that all of the website content is available in the navigator
- "{{ WEB_PROTOCOL }}://*.{{ PRIMARY_DOMAIN }}"
flags:
style-src:
unsafe-inline: true
unsafe-inline: true
script-src:
unsafe-eval: true
unsafe-eval: true
script-src-elem:
unsafe-inline: true
unsafe-inline: true
domains:
canonical:
- "slides.{{ PRIMARY_DOMAIN }}"

View File

@@ -1,8 +1,8 @@
galaxy_info:
author: "Kevin Veen-Birkenbach"
description: "An interactive presentation platform focused on guiding end-users through the practical use of the Infinito.Nexus software. Designed to demonstrate features, workflows, and real-world applications for Administrators, Developers, End-Users, Businesses, and Investors."
license: "Infinito.Nexus NonCommercial License"
license_url: "https://s.infinito.nexus/license"
author: "Kevin Veen-Birkenbach"
description: "An interactive presentation platform focused on guiding end-users through the practical use of the Infinito.Nexus software. Designed to demonstrate features, workflows, and real-world applications for Administrators, Developers, End-Users, Businesses, and Investors."
license: "Infinito.Nexus NonCommercial License"
license_url: "https://s.infinito.nexus/license"
company: |
Kevin Veen-Birkenbach
Consulting & Coaching Solutions

View File

@@ -13,4 +13,3 @@
{% include 'roles/docker-container/templates/networks.yml.j2' %}
{% include 'roles/docker-compose/templates/networks.yml.j2' %}

View File

@@ -1,9 +1,9 @@
features:
matomo: true
css: true
desktop: true
javascript: false
logout: false
matomo: true
css: true
desktop: true
javascript: false
logout: false
server:
domains:
canonical:
@@ -19,10 +19,11 @@ server:
connect-src:
- "{{ WEB_PROTOCOL }}://*.{{ PRIMARY_DOMAIN }}"
- "{{ WEB_PROTOCOL }}://{{ PRIMARY_DOMAIN }}"
- "https://cdn.jsdelivr.net"
script-src-elem:
- https://cdn.jsdelivr.net
- "https://cdn.jsdelivr.net"
style-src-elem:
- https://cdn.jsdelivr.net
- "https://cdn.jsdelivr.net"
frame-ancestors:
- "{{ WEB_PROTOCOL }}://<< defaults_applications[web-app-keycloak].server.domains.canonical[0] >>"

View File

@@ -21,11 +21,6 @@
- name: "load docker, proxy for '{{ application_id }}'"
include_role:
name: sys-stk-full-stateless
vars:
aca_origin: "'{{ domains | get_url('web-svc-logout', WEB_PROTOCOL) }}' always"
aca_credentials: "'true' always"
aca_methods: "'GET, OPTIONS' always"
aca_headers: "'Accept, Authorization' always"
- name: Create symbolic link from .env file to repository
file:

View File

@@ -8,7 +8,11 @@ location = /logout {
proxy_http_version 1.1;
{# CORS headers allow your central page to call this #}
{% include 'roles/sys-svc-proxy/templates/headers/access_control_allow.conf.j2' %}
{%- set aca_origin = "'{{ domains | get_url('web-svc-logout', WEB_PROTOCOL) }}' always" -%}
{%- set aca_credentials = "'true' always" -%}
{%- set aca_methods = "'GET, OPTIONS' always" -%}
{%- set aca_headers = "'Accept, Authorization' always" -%}
{%- include 'roles/sys-svc-proxy/templates/headers/access_control_allow.conf.j2' -%}
{# Disable caching absolutely #}
add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0" always;

View File

@@ -28,14 +28,17 @@ BUILTIN_FILTERS: Set[str] = {
"int", "join", "last", "length", "list", "lower", "map", "min", "max", "random",
"reject", "rejectattr", "replace", "reverse", "round", "safe", "select",
"selectattr", "slice", "sort", "string", "striptags", "sum", "title", "trim",
"truncate", "unique", "upper", "urlencode", "urlize", "wordcount", "xmlattr",
"truncate", "unique", "upper", "urlencode", "urlize", "wordcount", "xmlattr","contains",
# Common Ansible filters (subset, extend as needed)
"b64decode", "b64encode", "basename", "dirname", "from_json", "to_json",
"from_yaml", "to_yaml", "combine", "difference", "intersect",
"flatten", "zip", "regex_search", "regex_replace", "bool",
"type_debug", "json_query", "mandatory", "hash", "checksum",
"lower", "upper", "capitalize", "unique", "dict2items", "items2dict", "password_hash", "path_join", "product", "quote", "split", "ternary", "to_nice_yaml", "tojson",
"lower", "upper", "capitalize", "unique", "dict2items", "items2dict",
"password_hash", "path_join", "product", "quote", "split", "ternary", "to_nice_yaml",
"tojson", "to_nice_json",
# Date/time-ish
"strftime",