diff --git a/cli/utils/manager/inventory.py b/cli/utils/manager/inventory.py index 4aaa992a..ec74b5ba 100644 --- a/cli/utils/manager/inventory.py +++ b/cli/utils/manager/inventory.py @@ -42,11 +42,14 @@ class InventoryManager: data = YamlHandler.load_yaml(vars_file) # Check if 'central-database' is enabled in the features section of data - if "features" in data and \ - "central_database" in data["features"] and \ - data["features"]["central_database"]: - # Add 'central_database' value (password) to credentials - target.setdefault("credentials", {})["database_password"] = self.generate_value("alphanumeric") + if "features" in data: + if "central_database" in data["features"] and \ + data["features"]["central_database"]: + # Add 'central_database' value (password) to credentials + target.setdefault("credentials", {})["database_password"] = self.generate_value("alphanumeric") + if "oauth2" in data["features"] and \ + data["features"]["oauth2"]: + target.setdefault("credentials", {})["oauth2"] = self.generate_value("random_hex_16") # Apply recursion only for the `credentials` section self.recurse_credentials(self.schema, target) @@ -102,7 +105,41 @@ class InventoryManager: return ''.join(secrets.choice(characters) for _ in range(length)) def generate_value(self, algorithm: str) -> str: - """Generate a value based on the provided algorithm.""" + """ + Generate a random secret value according to the specified algorithm. + + Supported algorithms: + • "random_hex" + – Returns a 64-byte (512-bit) secure random string, encoded as 128 hexadecimal characters. + – Use when you need maximum entropy in a hex-only format. + + • "sha256" + – Generates 32 random bytes, hashes them with SHA-256, and returns a 64-character hex digest. + – Good for when you want a fixed-length (256-bit) hash output. + + • "sha1" + – Generates 20 random bytes, hashes them with SHA-1, and returns a 40-character hex digest. + – Only use in legacy contexts; SHA-1 is considered weaker than SHA-256. + + • "bcrypt" + – Creates a random 16-byte URL-safe password, then applies a bcrypt hash. + – Suitable for storing user-style passwords where bcrypt verification is needed. + + • "alphanumeric" + – Produces a 64-character string drawn from [A–Z, a–z, 0–9]. + – Offers ≈380 bits of entropy; human-friendly charset. + + • "base64_prefixed_32" + – Generates 32 random bytes, encodes them in Base64, and prefixes the result with "base64:". + – Useful when downstream systems expect a Base64 format. + + • "random_hex_16" + – Returns 16 random bytes (128 bits) encoded as 32 hexadecimal characters. + – Handy for shorter tokens or salts. + + Returns: + A securely generated string according to the chosen algorithm. + """ if algorithm == "random_hex": return secrets.token_hex(64) diff --git a/group_vars/all/08_ports.yml b/group_vars/all/08_ports.yml index f205481a..79397b2f 100644 --- a/group_vars/all/08_ports.yml +++ b/group_vars/all/08_ports.yml @@ -14,6 +14,7 @@ ports: phpldapadmin: 4186 fusiondirectory: 4187 gitea: 4188 + snipe-it: 4189 ldap: ldap: 389 http: @@ -59,6 +60,7 @@ ports: espocrm: 8040 syncope: 8041 collabora: 8042 + mobilizon: 8043 bigbluebutton: 48087 # This port is predefined by bbb. @todo Try to change this to a 8XXX port # Ports which are exposed to the World Wide Web public: diff --git a/group_vars/all/09_networks.yml b/group_vars/all/09_networks.yml index 39335f0e..e9da8c3e 100644 --- a/group_vars/all/09_networks.yml +++ b/group_vars/all/09_networks.yml @@ -14,8 +14,8 @@ defaults_networks: subnet: 192.168.101.16/28 baserow: subnet: 192.168.101.32/28 - # Free: - # subnet: 192.168.101.48/28 + mobilizon: + subnet: 192.168.101.48/28 bluesky: subnet: 192.168.101.64/28 friendica: diff --git a/group_vars/all/12_oidc.yml b/group_vars/all/12_oidc.yml new file mode 100644 index 00000000..1e310cae --- /dev/null +++ b/group_vars/all/12_oidc.yml @@ -0,0 +1,33 @@ +############################################# +### Identity and Access Management (IAM) ### +############################################# + +############################################# +### OIDC ### +############################################# +# @see https://en.wikipedia.org/wiki/OpenID_Connect + +## Helper Variables: +_oidc_client_realm: "{{ oidc.client.realm if oidc.client is defined and oidc.client.realm is defined else primary_domain }}" +_oidc_client_issuer_url: "{{ web_protocol }}://{{domains | get_domain('keycloak')}}/realms/{{_oidc_client_realm}}" + +defaults_oidc: + client: + id: "{{primary_domain}}" # Client identifier, typically matching your primary domain +# secret: # Client secret for authenticating with the OIDC provider (set in the inventory file). Recommend greater then 32 characters + realm: "{{_oidc_client_realm}}" # The realm to which the client belongs in the OIDC provider + issuer_url: "{{_oidc_client_issuer_url}}" # Base URL of the OIDC provider (issuer) + discovery_document: "{{_oidc_client_issuer_url}}/.well-known/openid-configuration" # URL for fetching the provider's configuration details + authorize_url: "{{_oidc_client_issuer_url}}/protocol/openid-connect/auth" # Endpoint to start the authorization process + token_url: "{{_oidc_client_issuer_url}}/protocol/openid-connect/token" # Endpoint to exchange authorization codes for tokens (note: 'token_url' may be a typo for 'token_url') + user_info_url: "{{_oidc_client_issuer_url}}/protocol/openid-connect/userinfo" # Endpoint to retrieve user information + logout_url: "{{_oidc_client_issuer_url}}/protocol/openid-connect/logout" # Endpoint to log out the user + change_credentials: "{{_oidc_client_issuer_url}}account/account-security/signing-in" # URL for managing or changing user credentials + certs: "{{_oidc_client_issuer_url}}/protocol/openid-connect/certs" # JSON Web Key Set (JWKS) + button_text: "SSO Login ({{primary_domain | upper}})" # Default button text + attributes: + # Attribut to identify the user + username: "preferred_username" + given_name: "givenName" + family_name: "surname" + email: "email" \ No newline at end of file diff --git a/group_vars/all/12_iam.yml b/group_vars/all/13_ldap.yml similarity index 65% rename from group_vars/all/12_iam.yml rename to group_vars/all/13_ldap.yml index 076899c5..8128ea6d 100644 --- a/group_vars/all/12_iam.yml +++ b/group_vars/all/13_ldap.yml @@ -1,36 +1,3 @@ -############################################# -### Identity and Access Management (IAM) ### -############################################# - -############################################# -### OIDC ### -############################################# -# @see https://en.wikipedia.org/wiki/OpenID_Connect - -## Helper Variables: -_oidc_client_realm: "{{ oidc.client.realm if oidc.client is defined and oidc.client.realm is defined else primary_domain }}" -_oidc_client_issuer_url: "{{ web_protocol }}://{{domains | get_domain('keycloak')}}/realms/{{_oidc_client_realm}}" - -defaults_oidc: - client: - id: "{{primary_domain}}" # Client identifier, typically matching your primary domain -# secret: # Client secret for authenticating with the OIDC provider (set in the inventory file). Recommend greater then 32 characters - realm: "{{_oidc_client_realm}}" # The realm to which the client belongs in the OIDC provider - issuer_url: "{{_oidc_client_issuer_url}}" # Base URL of the OIDC provider (issuer) - discovery_document: "{{_oidc_client_issuer_url}}/.well-known/openid-configuration" # URL for fetching the provider's configuration details - authorize_url: "{{_oidc_client_issuer_url}}/protocol/openid-connect/auth" # Endpoint to start the authorization process - token_url: "{{_oidc_client_issuer_url}}/protocol/openid-connect/token" # Endpoint to exchange authorization codes for tokens (note: 'token_url' may be a typo for 'token_url') - user_info_url: "{{_oidc_client_issuer_url}}/protocol/openid-connect/userinfo" # Endpoint to retrieve user information - logout_url: "{{_oidc_client_issuer_url}}/protocol/openid-connect/logout" # Endpoint to log out the user - change_credentials: "{{_oidc_client_issuer_url}}account/account-security/signing-in" # URL for managing or changing user credentials - certs: "{{_oidc_client_issuer_url}}/protocol/openid-connect/certs" # JSON Web Key Set (JWKS) - button_text: "SSO Login ({{primary_domain | upper}})" # Default button text - attributes: - # Attribut to identify the user - username: "preferred_username" - given_name: "givenName" - family_name: "surname" - email: "email" ############################################# ### LDAP ### diff --git a/group_vars/all/13_storage.yml b/group_vars/all/16_storage.yml similarity index 100% rename from group_vars/all/13_storage.yml rename to group_vars/all/16_storage.yml diff --git a/roles/cleanup-domains/tasks/remove_deprecated_nginx_configs.yml b/roles/cleanup-domains/tasks/remove_deprecated_nginx_configs.yml index dd0f289b..0b611346 100644 --- a/roles/cleanup-domains/tasks/remove_deprecated_nginx_configs.yml +++ b/roles/cleanup-domains/tasks/remove_deprecated_nginx_configs.yml @@ -1,7 +1,7 @@ --- - name: Find matching nginx configs for {{ domain }} ansible.builtin.find: - paths: /etc/nginx/conf.d/http/servers + paths: "{{ nginx.directories.http.servers }}" patterns: "*.{{ domain }}.conf" register: find_result @@ -15,6 +15,6 @@ - name: Remove exact nginx config for {{ domain }} ansible.builtin.file: - path: "/etc/nginx/conf.d/http/servers/{{ domain }}.conf" + path: "{{ nginx.directories.http.servers }}{{ domain }}.conf" state: absent notify: restart nginx \ No newline at end of file diff --git a/roles/docker-akaunting/templates/docker-compose.yml.j2 b/roles/docker-akaunting/templates/docker-compose.yml.j2 index fb2eb9f5..9853c0be 100644 --- a/roles/docker-akaunting/templates/docker-compose.yml.j2 +++ b/roles/docker-akaunting/templates/docker-compose.yml.j2 @@ -6,7 +6,7 @@ services: {% include 'roles/docker-compose/templates/services/base.yml.j2' %} - image: "{{ applications[application_id].images.akaunting }}" + image: "{{ applications[application_id].images[application_id] }}" build: context: . ports: diff --git a/roles/docker-akaunting/templates/env.j2 b/roles/docker-akaunting/templates/env.j2 index c78c4302..9c15d7f8 100644 --- a/roles/docker-akaunting/templates/env.j2 +++ b/roles/docker-akaunting/templates/env.j2 @@ -1,5 +1,5 @@ # You should change this to match your reverse proxy DNS name and protocol -APP_URL=https://{{domains | get_domain(application_id)}} +APP_URL={{ web_protocol }}://{{domains | get_domain(application_id)}} LOCALE={{ HOST_LL }} # Don't change this unless you rename your database container or use rootless podman, in case of using rootless podman you should set it to 127.0.0.1 (NOT localhost) diff --git a/roles/docker-gitea/meta/schema.yml b/roles/docker-gitea/meta/schema.yml index 7eecfa2f..075c0f24 100644 --- a/roles/docker-gitea/meta/schema.yml +++ b/roles/docker-gitea/meta/schema.yml @@ -1,5 +1 @@ -credentials: - oauth2_proxy_cookie_secret: - description: "Secret used to encrypt cookies for the OAuth2 proxy (hex-encoded, 16 bytes)" - algorithm: "random_hex_16" - validation: "^[a-f0-9]{32}$" \ No newline at end of file +credentials: \ No newline at end of file diff --git a/roles/docker-lam/meta/schema.yml b/roles/docker-lam/meta/schema.yml index 67ad8e5d..e94bb798 100644 --- a/roles/docker-lam/meta/schema.yml +++ b/roles/docker-lam/meta/schema.yml @@ -1,9 +1,4 @@ credentials: - oauth2_proxy_cookie_secret: - description: "Secret used to encrypt cookies for the OAuth2 proxy (hex-encoded, 16 bytes)" - algorithm: "random_hex_16" - validation: "^[a-f0-9]{32}$" - administrator_password: description: "Initial password for the LAM administrator" algorithm: "sha256" diff --git a/roles/docker-mastodon/templates/docker-compose.yml.j2 b/roles/docker-mastodon/templates/docker-compose.yml.j2 index 5fce95a8..a1eb720e 100644 --- a/roles/docker-mastodon/templates/docker-compose.yml.j2 +++ b/roles/docker-mastodon/templates/docker-compose.yml.j2 @@ -5,7 +5,7 @@ services: {% include 'roles/docker-central-database/templates/services/' + database_type + '.yml.j2' %} web: - image: "{{ applications[application_id].images.mastodon }}" + image: "{{ applications[application_id].images[application_id] }}" {% include 'roles/docker-compose/templates/services/base.yml.j2' %} command: bash -c "rm -f /mastodon/tmp/pids/server.pid; bundle exec rails s -p 3000" healthcheck: diff --git a/roles/docker-mastodon/templates/env.j2 b/roles/docker-mastodon/templates/env.j2 index 3b9f2929..d45b8d3f 100644 --- a/roles/docker-mastodon/templates/env.j2 +++ b/roles/docker-mastodon/templates/env.j2 @@ -33,11 +33,11 @@ ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY= {{applications.mastodon.credenti ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT= {{applications.mastodon.credentials.active_record_encryption_key_derivation_salt}} ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY= {{applications.mastodon.credentials.active_record_encryption_primary_key}} -DB_HOST={{database_host}} -DB_PORT={{database_port}} -DB_NAME={{database_name}} -DB_USER={{database_username}} -DB_PASS={{database_password}} +DB_HOST={{ database_host }} +DB_PORT={{ database_port }} +DB_NAME={{ database_name }} +DB_USER={{ database_username }} +DB_PASS={{ database_password }} REDIS_HOST=redis REDIS_PORT=6379 diff --git a/roles/docker-matomo/meta/schema.yml b/roles/docker-matomo/meta/schema.yml index 069ecb74..2d938afe 100644 --- a/roles/docker-matomo/meta/schema.yml +++ b/roles/docker-matomo/meta/schema.yml @@ -1,11 +1,5 @@ credentials: - auth_token: description: "Authentication token for the Matomo HTTP API (used for automation and integrations)" algorithm: "sha256" - validation: "^[a-f0-9]{64}$" - - oauth2_proxy_cookie_secret: - description: "Secret used to encrypt cookies for the OAuth2 proxy (hex-encoded, 16 bytes)" - algorithm: "random_hex_16" - validation: "^[a-f0-9]{32}$" \ No newline at end of file + validation: "^[a-f0-9]{64}$" \ No newline at end of file diff --git a/roles/docker-matomo/templates/docker-compose.yml.j2 b/roles/docker-matomo/templates/docker-compose.yml.j2 index a473ad6a..c7f858eb 100644 --- a/roles/docker-matomo/templates/docker-compose.yml.j2 +++ b/roles/docker-matomo/templates/docker-compose.yml.j2 @@ -4,9 +4,9 @@ services: application: {% include 'roles/docker-compose/templates/services/base.yml.j2' %} - image: "{{ applications[application_id].images.matomo }}" + image: "{{ applications[application_id].images[application_id] }}" ports: - - "127.0.0.1:{{ports.localhost.http.matomo}}:80" + - "127.0.0.1:{{ports.localhost.http[application_id]}}:80" volumes: - data:/var/www/html {% include 'templates/docker/container/depends-on-just-database.yml.j2' %} diff --git a/roles/docker-mobilizon/README.md b/roles/docker-mobilizon/README.md new file mode 100644 index 00000000..1b502168 --- /dev/null +++ b/roles/docker-mobilizon/README.md @@ -0,0 +1,29 @@ +# Mobilizon + +## Description + +Experience Mobilizon, an open-source event management platform that empowers communities to create, manage, and attend events with ease. Mobilizon puts privacy and decentralization first, giving you full control over your data and how you engage with your audience. + +## Overview + +This role deploys Mobilizon using Docker, automating the setup of your event management platform along with its underlying database. With support for health checks, persistent storage for uploads and configuration, and seamless integration with an Nginx reverse proxy, Mobilizon is configured to provide reliable and scalable event hosting for your community. + +## Features + +- **Event Scheduling:** Create and manage events with rich metadata and RSVP functionality. +- **Community-Driven:** Foster connections with built-in discussion and follow features for organizers and participants. +- **Privacy-First:** Self-hosted solution ensures data ownership and GDPR-compliance. +- **Customizable Setup:** Configure database connections, instance settings, and admin credentials via environment variables and a TOML configuration file. +- **Scalable Deployment:** Use Docker to ensure your event platform grows seamlessly with your community’s needs. + +## Additional Resources + +- [Mobilizon Official Website](https://mobilizon.org) + +## Credits + +Developed and maintained by **Kevin Veen-Birkenbach**. +Learn more at [veen.world](https://www.veen.world). + +Part of the [CyMaIS Project](https://github.com/kevinveenbirkenbach/cymais) +Licensed under [CyMaIS NonCommercial License (CNCL)](https://s.veen.world/cncl) diff --git a/roles/docker-mobilizon/Todo.md b/roles/docker-mobilizon/Todo.md new file mode 100644 index 00000000..4aaa0e2d --- /dev/null +++ b/roles/docker-mobilizon/Todo.md @@ -0,0 +1,2 @@ +# Todo +- Implement \ No newline at end of file diff --git a/roles/docker-mobilizon/meta/main.yml b/roles/docker-mobilizon/meta/main.yml new file mode 100644 index 00000000..f5e7f88a --- /dev/null +++ b/roles/docker-mobilizon/meta/main.yml @@ -0,0 +1,22 @@ +--- +galaxy_info: + author: "Kevin Veen-Birkenbach" + description: "Experience Mobilizon, an open-source event management platform that empowers communities to create, manage, and attend events with ease, prioritizing privacy and decentralization." + license: "CyMaIS NonCommercial License (CNCL)" + license_url: "https://s.veen.world/cncl" + company: | + Kevin Veen-Birkenbach + Consulting & Coaching Solutions + https://www.veen.world + galaxy_tags: + - mobilizon + - docker + - event-management + - open-source + repository: "https://s.veen.world/cymais" + issue_tracker_url: "https://s.veen.world/cymaisissues" + documentation: "https://s.veen.world/cymais" + logo: + class: "fa-solid fa-calendar-days" + run_after: + - "docker-postgres" diff --git a/roles/docker-mobilizon/meta/schema.yml b/roles/docker-mobilizon/meta/schema.yml new file mode 100644 index 00000000..e05fc6d5 --- /dev/null +++ b/roles/docker-mobilizon/meta/schema.yml @@ -0,0 +1,9 @@ +credentials: + secret_key_base: + description: "Secret key base used to generate secrets for encrypting and signing data" + algorithm: "alphanumeric" + validation: "^[A-Za-z0-9]{64}$" + secret_key: + description: "Secret key used as a base to generate JWT tokens" + algorithm: "alphanumeric" + validation: "^[A-Za-z0-9]{64}$" diff --git a/roles/docker-mobilizon/tasks/main.yml b/roles/docker-mobilizon/tasks/main.yml new file mode 100644 index 00000000..0443f5d8 --- /dev/null +++ b/roles/docker-mobilizon/tasks/main.yml @@ -0,0 +1,13 @@ +--- +- name: "include docker-central-database" + include_role: + name: docker-central-database + +- name: "include role nginx-domain-setup for {{application_id}}" + include_role: + name: nginx-domain-setup + vars: + domain: "{{ domains | get_domain(application_id) }}" + http_port: "{{ ports.localhost.http[application_id] }}" + +- include_tasks: "{{ playbook_dir }}/roles/docker-compose/tasks/create-files.yml" diff --git a/roles/docker-mobilizon/templates/docker-compose.yml.j2 b/roles/docker-mobilizon/templates/docker-compose.yml.j2 new file mode 100644 index 00000000..6f462e0b --- /dev/null +++ b/roles/docker-mobilizon/templates/docker-compose.yml.j2 @@ -0,0 +1,27 @@ +version: "3" + +services: + +{% include 'roles/docker-central-database/templates/services/' + database_type + '.yml.j2' %} + + mobilizon: + image: "{{ applications[application_id].images[application_id] }}" + volumes: + - uploads:/var/lib/mobilizon/uploads + # - ./config.exs:/etc/mobilizon/config.exs:ro + ports: + - "127.0.0.1:{{ ports.localhost.http[application_id] }}:{{ mobilizon_exposed_docker_port }}" + healthcheck: + test: ["CMD", "curl", "-f", "http://127.0.0.1:{{ mobilizon_exposed_docker_port }}"] + interval: 30s + timeout: 10s + retries: 3 +{% include 'roles/docker-compose/templates/services/base.yml.j2' %} +{% include 'templates/docker/container/depends-on-just-database.yml.j2' %} +{% include 'templates/docker/container/networks.yml.j2' %} + +{% include 'templates/docker/compose/volumes.yml.j2' %} + uploads: + +{% include 'templates/docker/compose/networks.yml.j2' %} + diff --git a/roles/docker-mobilizon/templates/env.j2 b/roles/docker-mobilizon/templates/env.j2 new file mode 100644 index 00000000..30a57c35 --- /dev/null +++ b/roles/docker-mobilizon/templates/env.j2 @@ -0,0 +1,129 @@ +# Copy this file to .env, then update it with your own settings + + +###################################################### +# Instance configuration # +###################################################### + +# The name for your instance +MOBILIZON_INSTANCE_NAME={{ applications[application_id].titel }} + +# Your domain +MOBILIZON_INSTANCE_HOST={{ domains | get_domain(application_id) }} + +# The IP to listen on (defaults to 0.0.0.0) +# MOBILIZON_INSTANCE_LISTEN_IP + +# The port to listen on (defaults to 4000). Point your reverse proxy on this port. +MOBILIZON_INSTANCE_PORT={{ mobilizon_exposed_docker_port }} + +# Whether registrations are opened or closed. Can be changed in the admin settings UI as well. +# Make sure to moderate actively your instance if registrations are opened. +MOBILIZON_INSTANCE_REGISTRATIONS_OPEN=false + +# From which email will the emails be sent +MOBILIZON_INSTANCE_EMAIL={{ users["no-reply"].email }} + +# To which email with the replies be sent +MOBILIZON_REPLY_EMAIL={{ users["administrator"].email }} + +# The loglevel setting. +# You can find accepted values here: https://hexdocs.pm/logger/Logger.html#module-levels +# Defaults to error +MOBILIZON_LOGLEVEL={% if enable_debug | bool %}debug{% else %}error{% endif %} + +###################################################### +# Database settings # +###################################################### + +# The values below will be given to both the PostGIS (PostgreSQL) and Mobilizon containers +# Use the next settings if you plan to use an existing external database + +# The Mobilizon Database username. Defaults to $POSTGRES_USER. +# Change if using an external database. +MOBILIZON_DATABASE_USERNAME={{ database_username }} + +# The Mobilizon Database password. Defaults to $POSTGRES_PASSWORD. +# Change if using an external database. +MOBILIZON_DATABASE_PASSWORD={{ database_password }} + +# The Mobilizon Database name. Defaults to $POSTGRES_DB. +# Change if using an external database. +MOBILIZON_DATABASE_DBNAME={{ database_name }} + +# The Mobilizon database host. Useful if using an external database. +MOBILIZON_DATABASE_HOST={{ database_host }} + +# The Mobilizon database port. Useful if using an external database. +MOBILIZON_DATABASE_PORT={{ database_port }} + +# Whether to use SSL to connect to the Mobilizon database. Useful if using an external database. +# MOBILIZON_DATABASE_SSL=false + +###################################################### +# Secrets # +###################################################### + +# A secret key used as a base to generate secrets for encrypting and signing data. +# Make sure it's long enough (~64 characters should be fine) +# You can run `openssl rand -base64 48` to generate such a secret +MOBILIZON_INSTANCE_SECRET_KEY_BASE={{ applications[application_id].secret_key_base }} + +# A secret key used as a base to generate JWT tokens +# Make sure it's long enough (~64 characters should be fine) +# You can run `openssl rand -base64 48` to generate such a secret +MOBILIZON_INSTANCE_SECRET_KEY={{ applications[application_id].secret_key }} + + +###################################################### +# Email settings # +###################################################### + +# The SMTP server +# Defaults to localhost +MOBILIZON_SMTP_SERVER={{system_email.host}} +MOBILIZON_SMTP_PORT={{system_email.port}} +MOBILIZON_SMTP_USERNAME={{ users['no-reply'].email }} +MOBILIZON_SMTP_PASSWORD={{ users['no-reply'].mailu_token }} + +# Whether to use SSL for SMTP. +# Boolean +# Defaults to false +MOBILIZON_SMTP_SSL=false + +# Whether to use TLS for SMTP. +# Allowed values: always (TLS), never (Clear) and if_available (STARTTLS) +# Make sure to match the port value as well +# Defaults to "if_available" +MOBILIZON_SMTP_TLS={% if system_email.tls %}TLS{% elif system_email.start_tls %}STARTTLS{% else %}Clear{% endif %} + +{% if applications | is_feature_enabled('oidc',application_id) %} +#################################### +# ▶️ Mobilizon OIDC Configuration +#################################### + +AUTHENTICATION_STRATEGIES=open_id_connect + +# Display name of the OIDC login button +UEBERAUTH_OPENID_CONNECT_DISPLAY_NAME="{{ oidc.button_text }}" + +# Use discovery to automatically fetch OIDC provider settings +UEBERAUTH_OPENID_CONNECT_DISCOVERY_DOCUMENT={{ oidc.client.discovery_document }} + +# OIDC OAuth2 client credentials +UEBERAUTH_OPENID_CONNECT_CLIENT_ID={{ oidc.client.id }} +UEBERAUTH_OPENID_CONNECT_CLIENT_SECRET={{ oidc.client.secret }} + +# Redirect URI for the OIDC callback +UEBERAUTH_OPENID_CONNECT_REDIRECT_URI={{ mobilizon_oidc_callback_url }} + +# Scope and response type for OIDC +UEBERAUTH_OPENID_CONNECT_SCOPE=openid email profile +UEBERAUTH_OPENID_CONNECT_RESPONSE_TYPE=code + +# Claim/field used to uniquely identify the user +UEBERAUTH_OPENID_CONNECT_UID_FIELD={{ oidc.attributes.username }} + +# Optional email verification behavior +UEBERAUTH_OPENID_CONNECT_ASSUME_EMAIL_IS_VERIFIED=true +{% endif %} diff --git a/roles/docker-mobilizon/vars/configuration.yml b/roles/docker-mobilizon/vars/configuration.yml new file mode 100644 index 00000000..dc215675 --- /dev/null +++ b/roles/docker-mobilizon/vars/configuration.yml @@ -0,0 +1,3 @@ +titel: "Mobilizon on {{ primary_domain | upper }}" +images: + mobilizon: "docker.io/framasoft/mobilizon" \ No newline at end of file diff --git a/roles/docker-mobilizon/vars/main.yml b/roles/docker-mobilizon/vars/main.yml new file mode 100644 index 00000000..3de0fda9 --- /dev/null +++ b/roles/docker-mobilizon/vars/main.yml @@ -0,0 +1,4 @@ +application_id: mobilizon +database_type: "mariadb" +mobilizon_oidc_callback_url: "{{ web_protocol }}://{{ domains | get_domain(application_id) }}/auth/openid_connect/callback" +mobilizon_exposed_docker_port: 4000 \ No newline at end of file diff --git a/roles/docker-openproject/meta/schema.yml b/roles/docker-openproject/meta/schema.yml index a5b619be..075c0f24 100644 --- a/roles/docker-openproject/meta/schema.yml +++ b/roles/docker-openproject/meta/schema.yml @@ -1,6 +1 @@ -credentials: - - oauth2_proxy_cookie_secret: - description: "Secret used to encrypt cookies for the OAuth2 proxy (hex-encoded, 16 bytes)" - algorithm: "random_hex_16" - validation: "^[a-f0-9]{32}$" \ No newline at end of file +credentials: \ No newline at end of file diff --git a/roles/docker-pgadmin/meta/schema.yml b/roles/docker-pgadmin/meta/schema.yml index 309a90ba..d341029b 100644 --- a/roles/docker-pgadmin/meta/schema.yml +++ b/roles/docker-pgadmin/meta/schema.yml @@ -1,8 +1,4 @@ credentials: - oauth2_proxy_cookie_secret: - description: "Secret used to encrypt cookies for the OAuth2 proxy (hex-encoded, 16 bytes)" - algorithm: "random_hex_16" - validation: "^[a-f0-9]{32}$" administrator_password: description: "Initial password for the pgAdmin administrator login" diff --git a/roles/docker-phpldapadmin/meta/schema.yml b/roles/docker-phpldapadmin/meta/schema.yml index 7eecfa2f..075c0f24 100644 --- a/roles/docker-phpldapadmin/meta/schema.yml +++ b/roles/docker-phpldapadmin/meta/schema.yml @@ -1,5 +1 @@ -credentials: - oauth2_proxy_cookie_secret: - description: "Secret used to encrypt cookies for the OAuth2 proxy (hex-encoded, 16 bytes)" - algorithm: "random_hex_16" - validation: "^[a-f0-9]{32}$" \ No newline at end of file +credentials: \ No newline at end of file diff --git a/roles/docker-phpldapadmin/templates/env.j2 b/roles/docker-phpldapadmin/templates/env.j2 index a932de14..76f83b7f 100644 --- a/roles/docker-phpldapadmin/templates/env.j2 +++ b/roles/docker-phpldapadmin/templates/env.j2 @@ -1,3 +1,3 @@ # @See https://github.com/leenooks/phpLDAPadmin/wiki/Docker-Container -APP_URL= https://{{domains | get_domain(application_id)}} +APP_URL= {{ web_protocol }}://{{domains | get_domain(application_id)}} LDAP_HOST= {{ldap.server.domain}} \ No newline at end of file diff --git a/roles/docker-phpmyadmin/meta/schema.yml b/roles/docker-phpmyadmin/meta/schema.yml index 7eecfa2f..075c0f24 100644 --- a/roles/docker-phpmyadmin/meta/schema.yml +++ b/roles/docker-phpmyadmin/meta/schema.yml @@ -1,5 +1 @@ -credentials: - oauth2_proxy_cookie_secret: - description: "Secret used to encrypt cookies for the OAuth2 proxy (hex-encoded, 16 bytes)" - algorithm: "random_hex_16" - validation: "^[a-f0-9]{32}$" \ No newline at end of file +credentials: \ No newline at end of file diff --git a/roles/docker-pixelfed/templates/env.j2 b/roles/docker-pixelfed/templates/env.j2 index 354c82f8..bf606b3a 100644 --- a/roles/docker-pixelfed/templates/env.j2 +++ b/roles/docker-pixelfed/templates/env.j2 @@ -3,9 +3,9 @@ APP_KEY={{applications[application_id].credentials.app_key}} ## General Settings APP_NAME="{{applications.pixelfed.titel}}" -APP_ENV=production +APP_ENV={{ CYMAIS_ENVIRONMENT | lower }} APP_DEBUG={{enable_debug | string | lower }} -APP_URL=https://{{domains | get_domain(application_id)}} +APP_URL={{ web_protocol }}://{{domains | get_domain(application_id)}} APP_DOMAIN="{{domains | get_domain(application_id)}}" ADMIN_DOMAIN="{{domains | get_domain(application_id)}}" SESSION_DOMAIN="{{domains | get_domain(application_id)}}" diff --git a/roles/docker-snipe-it/tasks/ldap.yml b/roles/docker-snipe-it/tasks/ldap.yml index f5fe3ba3..7e7d23f2 100644 --- a/roles/docker-snipe-it/tasks/ldap.yml +++ b/roles/docker-snipe-it/tasks/ldap.yml @@ -1,30 +1,85 @@ # @See https://raw.githubusercontent.com/snipe/snipe-it/master/app/Models/Setting.php --- -- name: "Enable und konfiguriere LDAP in Snipe-IT" - community.mysql.mysql_query: - login_host: "{{ database_host }}" - login_port: "{{ database_port }}" - login_user: "{{ database_username }}" - login_password: "{{ database_password }}" - db: "{{ database_name }}" - query: | - UPDATE settings SET - ldap_enabled = 1, - ldap_server = '{{ ldap.server.uri }}', - ldap_port = '{{ ldap.server.port }}', - ldap_uname = '{{ ldap.dn.administrator.data }}', - ldap_pword = '{{ ldap.bind_credential }}', - ldap_basedn = '{{ ldap.dn.root }}', - ldap_filter = '{{ ldap.filters.users.all }}', - ldap_username_field = '{{ ldap.attributes.user_id }}', - ldap_lname_field = '{{ ldap.attributes.surname }}', - ldap_fname_field = '{{ ldap.attributes.firstname }}', - ldap_auth_filter_query = '{{ ldap.filters.users.login }}', - ldap_version = 3, - ldap_pw_sync = 0, - is_ad = 0, - ad_domain = '', - ldap_default_group = '', - ldap_email = '{{ ldap.attributes.mail }}', - ldap_mem_lim = '{{ LDAP_MEM_LIM }}', - ldap_time_lim = '{{ LDAP_TIME_LIM }}'; +- name: "Wait until the Snipe-IT Login is available" + uri: + url: "{{ snipe_it_url }}/login" + method: GET + return_content: no + status_code: 200 + register: snipeit_admin_check + retries: 30 + delay: 5 + until: snipeit_admin_check.status == 200 + when: not ( applications | is_feature_enabled('oauth2', application_id)) + +- name: "Set all LDAP settings via Laravel Setting model (inside container as www-data)" + shell: | + docker-compose exec -T -e XDG_CONFIG_HOME=/tmp -u www-data application sh -c 'php artisan tinker << "EOF" + $s = \App\Models\Setting::getSettings(); + $s->ldap_enabled = 1; + $s->ldap_server = "{{ ldap.server.uri }}"; + $s->ldap_port = {{ ldap.server.port }}; + $s->ldap_uname = "{{ ldap.dn.administrator.data }}"; + $s->ldap_pword = "{{ ldap.bind_credential }}"; + $s->ldap_basedn = "{{ ldap.dn.root }}"; + $s->ldap_filter = "objectclass=inetOrgPerson"; + $s->ldap_username_field = "{{ ldap.attributes.user_id }}"; + $s->ldap_fname_field = "{{ ldap.attributes.firstname }}"; + $s->ldap_lname_field = "{{ ldap.attributes.surname }}"; + $s->ldap_auth_filter_query = "{{ ldap.filters.users.login }}"; + $s->ldap_version = 3; + $s->ldap_pw_sync = 0; + $s->is_ad = 0; + $s->ad_domain = ""; + $s->ldap_default_group = ""; + $s->ldap_email = "{{ ldap.attributes.mail }}"; + $s->custom_forgot_pass_url = "{{ ldap.attributes.mail }}"; + $s->save(); + EOF' + args: + #chdir: "/opt/docker/snipe-it/" + chdir: "{{ docker_compose.directories.instance }}" + register: ldap_tinker + failed_when: > + ldap_tinker.stdout_lines is not defined + or ldap_tinker.stdout_lines[0] != '= true' + changed_when: > + ldap_tinker.stdout_lines is defined + and ldap_tinker.stdout_lines[0] == '= true' + notify: docker compose up + +- name: "Clear Laravel config & cache (inside container as www-data)" + shell: | + docker-compose exec -T -u www-data application php artisan config:clear + docker-compose exec -T -u www-data application php artisan cache:clear + args: + #chdir: "/opt/docker/snipe-it/" + chdir: "{{ docker_compose.directories.instance }}" + notify: docker compose up + +#- name: "Enable und konfiguriere LDAP in Snipe-IT" +# community.mysql.mysql_query: +# login_host: "127.0.0.1" +# login_port: "{{ database_port }}" +# login_user: "{{ database_username }}" +# login_password: "{{ database_password }}" +# login_db: "{{ database_name }}" +# query: | +# UPDATE settings SET +# ldap_enabled = 1, +# ldap_server = '{{ ldap.server.uri }}', +# ldap_port = '{{ ldap.server.port }}', +# ldap_uname = '{{ ldap.dn.administrator.data }}', +# ldap_pword = '{{ ldap.bind_credential }}', +# ldap_basedn = '{{ ldap.dn.root }}', +# ldap_filter = '{{ ldap.filters.users.all }}', +# ldap_username_field = '{{ ldap.attributes.user_id }}', +# ldap_lname_field = '{{ ldap.attributes.surname }}', +# ldap_fname_field = '{{ ldap.attributes.firstname }}', +# ldap_auth_filter_query = '{{ ldap.filters.users.login }}', +# ldap_version = 3, +# ldap_pw_sync = 0, +# is_ad = 0, +# ad_domain = '', +# ldap_default_group = '', +# ldap_email = '{{ ldap.attributes.mail }}'; diff --git a/roles/docker-snipe-it/templates/docker-compose.yml.j2 b/roles/docker-snipe-it/templates/docker-compose.yml.j2 index 05527ec0..b6a3e2ba 100644 --- a/roles/docker-snipe-it/templates/docker-compose.yml.j2 +++ b/roles/docker-snipe-it/templates/docker-compose.yml.j2 @@ -4,6 +4,8 @@ services: {% include 'roles/docker-central-database/templates/services/' + database_type + '.yml.j2' %} +{% include 'roles/docker-oauth2-proxy/templates/container.yml.j2' %} + application: image: grokability/snipe-it:{{applications[application_id].version}} {% include 'roles/docker-compose/templates/services/base.yml.j2' %} diff --git a/roles/docker-snipe-it/templates/env.j2 b/roles/docker-snipe-it/templates/env.j2 index 15f41fa3..c2d69a46 100644 --- a/roles/docker-snipe-it/templates/env.j2 +++ b/roles/docker-snipe-it/templates/env.j2 @@ -1,11 +1,11 @@ # -------------------------------------------- # REQUIRED: BASIC APP SETTINGS # -------------------------------------------- -APP_ENV=production +APP_ENV={{ CYMAIS_ENVIRONMENT | lower }} APP_DEBUG={{enable_debug | string | lower }} # Please regenerate the APP_KEY value by calling `docker compose run --rm app php artisan key:generate --show`. Copy paste the value here APP_KEY={{applications[application_id].credentials.app_key}} -APP_URL=https://{{domains | get_domain(application_id)}} +APP_URL={{ snipe_it_url }} # https://en.wikipedia.org/wiki/List_of_tz_database_time_zones - TZ identifier APP_TIMEZONE='{{ HOST_TIMEZONE }}' APP_LOCALE={{ HOST_LL }} diff --git a/roles/docker-snipe-it/vars/configuration.yml b/roles/docker-snipe-it/vars/configuration.yml index 2f47e048..b8474a47 100644 --- a/roles/docker-snipe-it/vars/configuration.yml +++ b/roles/docker-snipe-it/vars/configuration.yml @@ -20,4 +20,10 @@ csp: unsafe-inline: true whitelist: font-src: - - "data:" \ No newline at end of file + - "data:" +oauth2_proxy: + application: "application" + port: "80" + acl: + blacklist: + - "/login" \ No newline at end of file diff --git a/roles/docker-snipe-it/vars/main.yml b/roles/docker-snipe-it/vars/main.yml index fe8e3893..1baf7652 100644 --- a/roles/docker-snipe-it/vars/main.yml +++ b/roles/docker-snipe-it/vars/main.yml @@ -1,3 +1,4 @@ application_id: "snipe-it" -database_password: "{{applications[application_id].credentials.database_password}}" -database_type: "mariadb" \ No newline at end of file +database_password: "{{ applications[application_id].credentials.database_password }}" +database_type: "mariadb" +snipe_it_url: "{{ web_protocol }}://{{domains | get_domain(application_id)}}" \ No newline at end of file diff --git a/roles/docker-yourls/meta/schema.yml b/roles/docker-yourls/meta/schema.yml index f5d68be5..5c58cdca 100644 --- a/roles/docker-yourls/meta/schema.yml +++ b/roles/docker-yourls/meta/schema.yml @@ -3,8 +3,3 @@ credentials: description: "Initial password for the YOURLS administrator account" algorithm: "sha256" validation: "^[a-f0-9]{64}$" - - oauth2_proxy_cookie_secret: - description: "Secret used to encrypt cookies for the OAuth2 proxy (hex-encoded, 16 bytes)" - algorithm: "random_hex_16" - validation: "^[a-f0-9]{32}$" diff --git a/tests/integration/test_docker_images_configuration.py b/tests/integration/test_docker_images_configuration.py index 59ff4a4d..ec50aa40 100644 --- a/tests/integration/test_docker_images_configuration.py +++ b/tests/integration/test_docker_images_configuration.py @@ -27,6 +27,8 @@ class TestDockerRoleImagesConfiguration(unittest.TestCase): try: config = yaml.safe_load(cfg_file.read_text("utf-8")) or {} + main_file = role_path / "vars" / "main.yml" + main = yaml.safe_load(main_file.read_text("utf-8")) or {} except yaml.YAMLError as e: errors.append(f"{role_path.name}: YAML parse error: {e}") continue @@ -50,20 +52,33 @@ class TestDockerRoleImagesConfiguration(unittest.TestCase): r'image:\s*["\']\{\{\s*applications\[application_id\]\.images\.' + re.escape(key) + r'\s*\}\}["\']' ) - found = False + # innerhalb Deines Loops + pattern2 = ( + r'image:\s*["\']\{\{\s*' # image: "{{ + r'applications\[\s*application_id\s*\]\.images' # applications[ application_id ].images + r'\[\s*application_id\s*\]\s*' # [ application_id ] + r'\}\}["\']' # }}" oder }}" + ) + + for tmpl_file in [ role_path / "templates" / "docker-compose.yml.j2", - role_path / "templates" / "env.j2" + role_path / "templates" / "env.j2", ]: - if tmpl_file.exists(): - content = tmpl_file.read_text("utf-8") - if re.search(pattern, content): - found = True - break - if not found: + if not tmpl_file.exists(): + continue + content = tmpl_file.read_text("utf-8") + if re.search(pattern, content): + break + if key == main.get('application_id') and re.search(pattern2, content): + break + else: + # Dieser Block wird nur ausgeführt, wenn kein `break` ausgelöst wurde errors.append( f"{role_path.name}: image key '{key}' is not referenced as " - f'image: \"{{{{ applications[application_id].images.{key} }}}}\" in docker-compose.yml.j2 or env.j2' + f"image: \"{{{{ applications[application_id].images.{key} }}}}\" or " + f"\"{{{{ applications[application_id].images[application_id] }}}}\" " + "in docker-compose.yml.j2 or env.j2" ) diff --git a/tests/integration/test_oauth2_proxy_ports.py b/tests/integration/test_oauth2_proxy_ports.py new file mode 100644 index 00000000..2d4c5000 --- /dev/null +++ b/tests/integration/test_oauth2_proxy_ports.py @@ -0,0 +1,58 @@ +import unittest +import yaml +from pathlib import Path + + +class TestOAuth2ProxyPorts(unittest.TestCase): + @classmethod + def setUpClass(cls): + # Set up root paths and load oauth2_proxy ports mapping + cls.ROOT = Path(__file__).parent.parent.parent.resolve() + cls.PORTS_FILE = cls.ROOT / 'group_vars' / 'all' / '08_ports.yml' + with cls.PORTS_FILE.open() as f: + data = yaml.safe_load(f) + cls.oauth2_ports = ( + data.get('ports', {}) + .get('localhost', {}) + .get('oauth2_proxy', {}) + ) + + def test_oauth2_feature_has_port_mapping(self): + # Iterate over each role directory + roles_dir = self.ROOT / 'roles' + for role_path in roles_dir.iterdir(): + if not role_path.is_dir(): + continue + with self.subTest(role=role_path.name): + # Check for configuration.yml + config_file = role_path / 'vars' / 'configuration.yml' + if not config_file.exists(): + self.skipTest(f"No configuration.yml for role {role_path.name}") + + config = yaml.safe_load(config_file.read_text()) or {} + if not config.get('features', {}).get('oauth2', False): + self.skipTest(f"OAuth2 not enabled for role {role_path.name}") + + # Load application_id from vars/main.yml + main_file = role_path / 'vars' / 'main.yml' + if not main_file.exists(): + self.fail(f"Missing vars/main.yml in role {role_path.name}") + main = yaml.safe_load(main_file.read_text()) or {} + app_id = main.get('application_id') + if not app_id: + self.fail(f"application_id not set in {main_file}") + + # Validate oauth2_ports structure + self.assertIsInstance(self.oauth2_ports, dict, + "oauth2_proxy ports mapping is not a dict") + + # Assert port mapping exists for the application + if app_id not in self.oauth2_ports: + self.fail( + f"Missing oauth2_proxy port mapping for application '{app_id}' " + f"in group_vars/all/08_ports.yml" + ) + + +if __name__ == '__main__': + unittest.main()