Added new user generation script and optimized mail config

This commit is contained in:
Kevin Veen-Birkenbach 2025-07-02 15:08:42 +02:00
parent 2ccfdf0de6
commit cb6fbba8f4
No known key found for this signature in database
GPG Key ID: 44D8F11FD62F878E
31 changed files with 281 additions and 144 deletions

View File

@ -1,6 +1,8 @@
ROLES_DIR := ./roles
APPLICATIONS_OUT := ./group_vars/all/03_applications.yml
APPLICATIONS_SCRIPT := ./cli/generate-applications-defaults.py
USERS_OUT := ./group_vars/all/10_users.yml
USERS_SCRIPT := ./cli/generate_users.py
INCLUDES_OUT := ./tasks/utils/docker-roles.yml
INCLUDES_SCRIPT := ./cli/generate_playbook.py
@ -11,6 +13,10 @@ build:
@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 users defaults → $(USERS_OUT) from roles in $(ROLES_DIR)"
@mkdir -p $(dir $(USERS_OUT))
python3 $(USERS_SCRIPT) --roles-dir $(ROLES_DIR) --output $(USERS_OUT)
@echo "✅ Users defaults written to $(USERS_OUT)\n"
@echo "🔧 Generating Docker role includes → $(INCLUDES_OUT)"
@mkdir -p $(dir $(INCLUDES_OUT))
python3 $(INCLUDES_SCRIPT) $(ROLES_DIR) -o $(INCLUDES_OUT) -p docker-

163
cli/generate_users.py Normal file
View File

@ -0,0 +1,163 @@
#!/usr/bin/env python3
import os
import sys
import argparse
import yaml
import glob
from collections import OrderedDict
def build_users(defs, primary_domain, start_id, become_pwd):
"""
Build user entries with auto-incremented uid/gid and default username/email.
Args:
defs (OrderedDict): Keys are user IDs, values are dicts with optional overrides.
primary_domain (str): e.g., 'example.com'.
start_id (int): Starting uid/gid (e.g., 1001).
become_pwd (str): Password string for all users.
Returns:
OrderedDict: Merged user definitions with full fields.
"""
users = OrderedDict()
next_id = start_id
for key, overrides in defs.items():
username = overrides.get('username', key)
email = overrides.get('email', f"{username}@{primary_domain}")
uid = overrides.get('uid', next_id)
gid = overrides.get('gid', next_id)
is_admin = overrides.get('is_admin', False)
entry = {
'username': username,
'email': email,
'password': become_pwd,
'uid': uid,
'gid': gid
}
if is_admin:
entry['is_admin'] = True
users[key] = entry
next_id += 1
return users
def load_user_defs(roles_dir):
"""
Scan all roles/*/vars/configuration.yml files and extract 'users:' sections.
Raises an exception if conflicting definitions are found.
Args:
roles_dir (str): Path to the directory containing role subdirectories.
Returns:
OrderedDict: Merged user definitions.
Raises:
ValueError: On invalid format or conflicting field values.
"""
pattern = os.path.join(roles_dir, '*/vars/configuration.yml')
files = sorted(glob.glob(pattern))
merged = OrderedDict()
for filepath in files:
with open(filepath, 'r') as f:
data = yaml.safe_load(f) or {}
users = data.get('users', {})
if not isinstance(users, dict):
continue
for key, overrides in users.items():
if not isinstance(overrides, dict):
raise ValueError(f"Invalid definition for user '{key}' in {filepath}")
if key not in merged:
merged[key] = overrides.copy()
else:
existing = merged[key]
for field, value in overrides.items():
if field in existing and existing[field] != value:
raise ValueError(
f"Conflict for user '{key}': field '{field}' has existing value "
f"'{existing[field]}', tried to set '{value}' in {filepath}"
)
existing.update(overrides)
return merged
def dictify(data):
"""
Recursively convert OrderedDict to regular dict before YAML dump.
"""
if isinstance(data, OrderedDict):
return {k: dictify(v) for k, v in data.items()}
if isinstance(data, dict):
return {k: dictify(v) for k, v in data.items()}
if isinstance(data, list):
return [dictify(v) for v in data]
return data
def parse_args():
parser = argparse.ArgumentParser(
description='Generate a users.yml by merging all roles/*/vars/configuration.yml users sections.'
)
parser.add_argument(
'--roles-dir', '-r', required=True,
help='Directory containing roles (e.g., roles/*/vars/configuration.yml).'
)
parser.add_argument(
'--output', '-o', required=True,
help='Path to the output YAML file (e.g., users.yml).'
)
parser.add_argument(
'--start-id', '-s', type=int, default=1001,
help='Starting uid/gid number (default: 1001).'
)
return parser.parse_args()
def main():
args = parse_args()
primary_domain = '{{ primary_domain }}'
become_pwd = '{{ ansible_become_password }}'
try:
user_defs = load_user_defs(args.roles_dir)
except ValueError as e:
print(f"Error merging user definitions: {e}", file=sys.stderr)
sys.exit(1)
users = build_users(
defs=user_defs,
primary_domain=primary_domain,
start_id=args.start_id,
become_pwd=become_pwd
)
default_users = {'default_users': users}
plain_data = dictify(default_users)
# Ensure strings are represented without Python-specific tags
yaml.SafeDumper.add_representer(
str,
lambda dumper, data: dumper.represent_scalar('tag:yaml.org,2002:str', data)
)
with open(args.output, 'w') as f:
yaml.safe_dump(
plain_data,
f,
default_flow_style=False,
sort_keys=False,
width=120
)
if __name__ == '__main__':
main()

View File

@ -1 +1,2 @@
*_applications.yml
*_users.yml

View File

@ -3,7 +3,7 @@ default_system_email:
domain: "{{primary_domain}}"
host: "mail.{{primary_domain}}"
port: 465
tls: true
tls: true # true for TLS and false for SSL
start_tls: false
smtp: true
# password: # Needs to be defined in inventory file

View File

@ -1,110 +1,50 @@
# Helper Variables
# Helper Variables for administrator
_users_administrator_username: "{{ users.administrator.username | default('administrator') }}"
_users_administrator_email: "{{ users.administrator.email | default(_users_administrator_username ~ '@' ~ primary_domain) }}"
# Helper Variables for bounce
_users_bounce_username: "{{ users.bounce.username | default('bounce') }}"
_users_bounce_email: "{{ users.bounce.email | default(_users_bounce_username ~ '@' ~ primary_domain) }}"
# Helper Variables for no-reply
_users_no_reply_username: "{{ users['no-reply'].username | default('no-reply') }}"
_users_no_reply_email: "{{ users['no-reply'].email | default(_users_no_reply_username ~ '@' ~ primary_domain) }}"
# Helper Variables for blackhole
_users_blackhole_username: "{{ users.blackhole.username | default('no-reply') }}"
_users_blackhole_email: "{{ users.blackhole.email | default(_users_blackhole_username ~ '@' ~ primary_domain) }}"
# Helper Variables for contact user
_users_contact_username: "{{ users.contact.username | default('contact') }}"
_users_contact_email: "{{ users.contact.email | default(_users_contact_username ~ '@' ~ primary_domain) }}"
# Helper Variables for support
_users_support_username: "{{ users.support.username | default('support') }}"
_users_support_email: "{{ users.support.email | default(_users_support_username ~ '@' ~ primary_domain) }}"
# Helper Variables for helpdesk
_users_helpdesk_username: "{{ users.helpdesk.username | default('helpdesk') }}"
_users_helpdesk_email: "{{ users.helpdesk.email | default(_users_helpdesk_username ~ '@' ~ primary_domain) }}"
# Extract SLD and TLD from primary_domain
_users_sld_username: "{{ primary_domain.split('.')[0] }}"
_users_sld_email: "{{ _users_sld_username ~ '@' ~ primary_domain }}"
_users_tld_username: "{{ primary_domain.split('.')[-1] }}"
_users_tld_email: "{{ _users_tld_username ~ '@' ~ primary_domain }}"
# Administrator
default_users:
# Credentials will be used as administration credentials for all applications and the system
administrator:
username: "{{_users_administrator_username}}" # Username of the administrator
email: "{{_users_administrator_email}}" # Email of the administrator
password: "{{ansible_become_password}}" # Example initialisation password needs to be set in inventory file
uid: 1001 # Posix User ID
gid: 1001 # Posix Group ID
is_admin: true # Define as admin user
# Account for Newsletter bouncing
bounce:
username: "{{ _users_bounce_username }}" # Bounce-handler account username
email: "{{ _users_bounce_email }}" # Email address for handling bounces
password: "{{ansible_become_password}}" # Example initialisation password needs to be set in inventory file
uid: 1002 # Posix User ID for bounce
gid: 1002 # Posix Group ID for bounce
# User to send System Emails from
no-reply:
username: "{{ _users_no_reply_username }}" # No-reply account username
email: "{{ _users_no_reply_email }}" # Email address for outgoing no-reply mails
password: "{{ansible_become_password}}" # Example initialisation password needs to be set in inventory file
uid: 1003 # Posix User ID for no-reply
gid: 1003 # Posix Group ID for no-reply
# Emails etc, what you send to this user will be forgetten
username: administrator
email: administrator@{{ primary_domain }}
password: '{{ ansible_become_password }}'
uid: 1001
gid: 1001
is_admin: true
blackhole:
username: "{{ _users_blackhole_username }}" # Blackhole account username
email: "{{ _users_blackhole_email }}" # Email address to which emails can be send which well be forgetten
password: "{{ansible_become_password}}" # Example initialisation password needs to be set in inventory file
uid: 1004 # Posix User ID for bounce
gid: 1004 # Posix Group ID for bounce
# The contact user account which clients and plattform users can contact
contact:
username: "{{ _users_contact_username }}" # Contact account username
email: "{{ _users_contact_email }}" # Email address to which initial contacct emails can be send
password: "{{ansible_become_password}}" # Example initialisation password needs to be set in inventory file
uid: 1005 # Posix User ID for bounce
gid: 1005 # Posix Group ID for bounce
# Support and Helpdesk accounts
support:
username: "{{ _users_support_username }}" # Support account username
email: "{{ _users_support_email }}" # Email for customer and platform support communication
password: "{{ ansible_become_password }}" # Example initialisation password needs to be set in inventory file
uid: 1006 # Posix User ID for support
gid: 1006 # Posix Group ID for support
helpdesk:
username: "{{ _users_helpdesk_username }}" # Helpdesk account username
email: "{{ _users_helpdesk_email }}" # Email for internal technical helpdesk communication
password: "{{ ansible_become_password }}" # Example initialisation password needs to be set in inventory file
uid: 1007 # Posix User ID for helpdesk
gid: 1007 # Posix Group ID for helpdesk
sld_user:
username: "{{ _users_sld_username }}" # Username based on SLD of the primary domain
email: "{{ _users_sld_email }}" # Email address with SLD username
password: "{{ ansible_become_password }}" # Init password from inventory
uid: 1008
gid: 1008
tld_user:
username: "{{ _users_tld_username }}" # Username based on TLD of the primary domain
email: "{{ _users_tld_email }}" # Email address with TLD username
password: "{{ ansible_become_password }}" # Init password from inventory
uid: 1009
gid: 1009
username: blackhole
email: blackhole@{{ primary_domain }}
password: '{{ ansible_become_password }}'
uid: 1002
gid: 1002
crm:
username: contact
email: contact@{{ primary_domain }}
password: '{{ ansible_become_password }}'
uid: 1003
gid: 1003
bounce:
username: bounce
email: bounce@{{ primary_domain }}
password: '{{ ansible_become_password }}'
uid: 1004
gid: 1004
newsletter:
username: newsletter
email: newsletter@{{ primary_domain }}
password: '{{ ansible_become_password }}'
uid: 1005
gid: 1005
no-reply:
username: no-reply
email: no-reply@{{ primary_domain }}
password: '{{ ansible_become_password }}'
uid: 1006
gid: 1006
sld:
username: '{{ primary_domain.split(''.'')[0] }}'
email: '{{ primary_domain.split(''.'')[0] }}@{{ primary_domain }}'
password: '{{ ansible_become_password }}'
uid: 1007
gid: 1007
tld:
username: '{{ primary_domain.split(''.'')[1] }}'
email: '{{ primary_domain.split(''.'')[1] }}@{{ primary_domain }}'
password: '{{ ansible_become_password }}'
uid: 1008
gid: 1008

View File

@ -1,8 +1,8 @@
images:
akaunting: "docker.io/akaunting/akaunting:latest"
company_name: "{{primary_domain}}"
company_email: "{{users.administrator.email}}"
setup_admin_email: "{{users.administrator.email}}"
company_email: "{{ users.administrator.email }}"
setup_admin_email: "{{ users.administrator.email }}"
features:
matomo: true
css: true

View File

@ -1,6 +1,6 @@
users:
administrator:
email: "{{users.administrator.email}}"
email: "administrator@{{ primary_domain }}"
images:
pds: "ghcr.io/bluesky-social/pds:latest"
pds:

View File

@ -0,0 +1,4 @@
users:
blackhole:
description: "Everything what will be send to this user will disapear"
username: "blackhole"

View File

View File

@ -48,7 +48,7 @@ env:
#DOCKER_USE_HOSTNAME: true
## on initial signup example 'user1@example.com,user2@example.com'
DISCOURSE_DEVELOPER_EMAILS: {{users.administrator.email}}
DISCOURSE_DEVELOPER_EMAILS: {{ users.administrator.email }}
# Set Logo
{% if service_provider.platform.logo | bool %}
@ -135,8 +135,8 @@ run:
- exec: rails r "SiteSetting.username_change_period = 0" # Deactivate changing of username
# Activate Administrator User
#- exec: printf '{{users.administrator.email}}\n{{users.administrator.password}}\n{{users.administrator.password}}\nY\n' | rake admin:create
#- exec: rails r "User.find_by_email('{{users.administrator.email}}').update(username: '{{users.administrator.username}}')"
#- exec: printf '{{ users.administrator.email }}\n{{users.administrator.password}}\n{{users.administrator.password}}\nY\n' | rake admin:create
#- exec: rails r "User.find_by_email('{{ users.administrator.email }}').update(username: '{{users.administrator.username}}')"
# The following code is just an inspiration, how to connect with the oidc account. as long as this is not set the admini account needs to be manually connected with oidc
# docker exec -it discourse_application rails runner "user = User.find_by_email('test@cymais.cloud'); UserAuth.create(user_id: user.id, provider: 'oidc', uid: 'eindeutige_oidc_id', info: { name: user.username, email: user.email })"

View File

@ -21,3 +21,4 @@ galaxy_info:
class: "fa-solid fa-phone"
run_after:
- docker-keycloak
- docker-mailu

View File

@ -50,12 +50,12 @@ ESPOCRM_CONFIG_LOGGER_ROTATION=false
# ------------------------------------------------
ESPOCRM_CONFIG_SMTP_SERVER={{ system_email.host }}
ESPOCRM_CONFIG_SMTP_PORT={{ system_email.port }}
ESPOCRM_CONFIG_SMTP_SECURITY=TLS
ESPOCRM_CONFIG_SMTP_SECURITY={{ "TLS" if system_email.start_tls else "SSL"}}
ESPOCRM_CONFIG_SMTP_AUTH=true
ESPOCRM_CONFIG_SMTP_USERNAME={{ users['no-reply'].email }}
ESPOCRM_CONFIG_SMTP_PASSWORD={{ users['no-reply'].mailu_token }}
ESPOCRM_CONFIG_SMTP_USERNAME={{ users['contact'].email }}
ESPOCRM_CONFIG_SMTP_PASSWORD={{ users['contact'].mailu_token }}
ESPOCRM_CONFIG_OUTBOUND_EMAIL_FROM_NAME={{ service_provider.company.titel }} - CRM
ESPOCRM_CONFIG_OUTBOUND_EMAIL_FROM_ADDRESS={{ users['no-reply'].email }}
ESPOCRM_CONFIG_OUTBOUND_EMAIL_FROM_ADDRESS={{ users['contact'].email }}
# ------------------------------------------------
# LDAP settings (optional)

View File

@ -2,10 +2,10 @@ images:
espocrm: "espocrm/espocrm:latest"
users:
administrator:
username: "{{ users.administrator.username }}"
email: "{{ users.administrator.email }}"
credentials:
username: "administrator"
crm:
description: "General contact account"
username: "contact"
features:
matomo: true
css: false
@ -26,6 +26,8 @@ csp:
connect-src:
- wss://espocrm.{{ primary_domain }}
- "data:"
frame-src:
- https://s.espocrm.com/
domains:
aliases:
- "crm.{{ primary_domain }}"

View File

@ -27,5 +27,5 @@ SMTP_STARTTLS= {{ 'on' if system_email.start_tls else 'off' }}
SMTP_FROM= no-reply
# Administrator Credentials
FRIENDICA_ADMIN_MAIL= {{users.administrator.email}}
MAILNAME= {{users.administrator.email}}
FRIENDICA_ADMIN_MAIL= {{ users.administrator.email }}
MAILNAME= {{ users.administrator.email }}

View File

@ -2,7 +2,7 @@ images:
keycloak: "quay.io/keycloak/keycloak:latest"
users:
administrator:
username: "{{users.administrator.username}}" # Administrator Username for Keycloak
username: "administrator"
import_realm: True # If True realm will be imported. If false skip.
credentials:
features:

View File

@ -8,7 +8,7 @@ hostname: "ldap" # Hostname of the LDAP Ser
webinterface: "lam" # The webinterface which should be used. Possible: lam and phpldapadmin
users:
administrator:
username: "{{users.administrator.username}}" # Administrator username
username: "administrator"
credentials:
features:
ldap: true

View File

@ -2,7 +2,11 @@ images:
listmonk: "listmonk/listmonk:latest"
users:
administrator:
username: "{{users.administrator.username}}" # Listmonk administrator account username
username: "administrator"
bounce:
username: "bounce"
newsletter:
username: "newsletter"
public_api_activated: False # Security hole. Can be used for spaming
version: "latest" # Docker Image version
features:

View File

@ -1,7 +1,7 @@
version: "2024.06" # Docker Image Version
users:
administrator:
email: "{{users.administrator.email}}" # Administrator Email for DNS Records
email: "administrator@{{ primary_domain }}" # Administrator Email for DNS Records
oidc:
email_by_username: true # If true, then the mail is set by the username. If wrong then the OIDC user email is used
enable_user_creation: true # Users will be created if not existing

View File

@ -9,7 +9,7 @@
- name: Create admin account via tootctl
command:
cmd: 'docker compose exec -u root web bash -c "RAILS_ENV=production bin/tootctl accounts create {{users.administrator.username}} --email {{users.administrator.email}} --confirmed --role Owner"'
cmd: 'docker compose exec -u root web bash -c "RAILS_ENV=production bin/tootctl accounts create {{users.administrator.username}} --email {{ users.administrator.email }} --confirmed --role Owner"'
chdir: "{{docker_compose.directories.instance}}"
register: tootctl_create
changed_when: tootctl_create.rc == 0

View File

@ -46,7 +46,7 @@ devture_traefik_config_entrypoint_web_forwardedHeaders_insecure: true
# you won't be required to define this variable (see `docs/configuring-playbook-ssl-certificates.md`).
#
# Example value: someone@example.com
devture_traefik_config_certificatesResolvers_acme_email: "{{users.administrator.email}}"
devture_traefik_config_certificatesResolvers_acme_email: "{{ users.administrator.email }}"
# A Postgres password to use for the superuser Postgres user (called `matrix` by default).
#

View File

@ -28,7 +28,7 @@ web_client_location: "{{ web_protocol }}://{{domains.matrix.element}}
public_baseurl: "{{ web_protocol }}://{{domains.matrix.synapse}}"
trusted_key_servers:
- server_name: "matrix.org"
admin_contact: 'mailto:{{users.administrator.email}}'
admin_contact: 'mailto:{{ users.administrator.email }}'
email:
smtp_host: "{{system_email.host}}"

View File

@ -1,10 +1,9 @@
images:
synapse: "matrixdotorg/synapse:latest"
element: "vectorim/element-web:latest"
# Set bridges
users:
administrator:
username: "{{users.administrator.username}}" # Accountname of the matrix admin
username: "administrator"
playbook_tags: "setup-all,start" # For the initial update use: install-all,ensure-matrix-users-created,start
server_name: "{{primary_domain}}" # Adress for the account names etc.
synapse:

View File

@ -1,8 +1,7 @@
site_titel: "Academy on {{primary_domain}}"
users:
administrator:
username: "{{users.administrator.username}}"
email: "{{users.administrator.email}}"
username: "administrator"
version: "4.5" # Latest LTS - Necessary for OIDC
features:
matomo: true

View File

@ -20,7 +20,7 @@ SMTP_NAME= {{ users['no-reply'].email }}
SMTP_PASSWORD= {{ users['no-reply'].mailu_token }}
# Email from configuration
MAIL_FROM_ADDRESS= "no-reply"
MAIL_FROM_ADDRESS= "{{ users['no-reply'].username }}"
MAIL_DOMAIN= "{{system_email.domain}}"
# Initial Admin Data

View File

@ -29,7 +29,9 @@ features:
central_database: true
users:
administrator:
username: "{{users.administrator.username}}"
username: "administrator"
no-reply:
username: "no-reply"
default_quota: '1000000000' # Quota to assign if no quota is specified in the OIDC response (bytes)
legacy_login_mask:
enabled: False # If true, then legacy login mask is shown. Otherwise just SSO

View File

@ -3,7 +3,7 @@ server_mode: False # If true then the p
master_password_required: True # Master password is required. Recommended True. False is a security risk.
users:
administrator:
email: "{{ users.administrator.email }}" # Initial login email address
email: "administrator@{{ primary_domain }}"
oauth2_proxy:
application: "application"
port: "80"

View File

@ -1,8 +1,8 @@
title: "Blog" # Wordpress titel
users: # Credentials
administrator: # Wordpress administrator
username: "{{users.administrator.username}}" # Username of the wordpress administrator
email: "{{users.administrator.email}}" # Email of the wordpress adminsitrator
username: "administrator"
email: "administrator@{{ primary_domain }}"
plugins:
wp-discourse:
enabled: "{{ 'discourse' in group_names | lower }}"

View File

@ -1,6 +1,6 @@
users:
administrator:
username: "{{users.administrator.username}}"
username: "administrator"
version: "latest"
oauth2_proxy:
application: "application"

View File

@ -0,0 +1,7 @@
users:
sld:
description: "Auto Generated Account to reserve the SLD"
username: "{{ primary_domain.split('.')[0] }}"
tld:
description: "Auto Generated Account to reserve the TLD"
username: "{{ primary_domain.split('.')[1] }}"

View File

@ -1,7 +1,7 @@
#!/bin/bash
/usr/bin/sendmail -t <<ERRMAIL
To: {{users.administrator.email}}
To: {{ users.administrator.email }}
From: systemd <{{ users['no-reply'].email }}>
Subject: $1
Content-Transfer-Encoding: 8bit

View File

@ -0,0 +1,9 @@
users:
administrator:
description: "System Administrator"
username: "administrator"
email: "administrator@{{ primary_domain }}"
password: "{{ ansible_become_password }}"
uid: 1001
gid: 1001
is_admin: true