diff --git a/group_vars/all/09_networks.yml b/group_vars/all/09_networks.yml index 77a66010..7e889504 100644 --- a/group_vars/all/09_networks.yml +++ b/group_vars/all/09_networks.yml @@ -118,6 +118,8 @@ defaults_networks: subnet: 192.168.104.80/28 web-svc-onlyoffice: subnet: 192.168.104.96/28 + web-app-suitecrm: + subnet: 192.168.104.112/28 # /24 Networks / 254 Usable Clients web-app-bigbluebutton: diff --git a/group_vars/all/10_ports.yml b/group_vars/all/10_ports.yml index a9417c08..3410ec8b 100644 --- a/group_vars/all/10_ports.yml +++ b/group_vars/all/10_ports.yml @@ -83,6 +83,7 @@ ports: web-app-mini-qr: 8059 web-app-shopware: 8060 web-svc-onlyoffice: 8061 + web-app-suitecrm: 8062 web-app-bigbluebutton: 48087 # This port is predefined by bbb. @todo Try to change this to a 8XXX port public: # The following ports should be changed to 22 on the subdomain via stream mapping diff --git a/roles/web-app-suitecrm/README.md b/roles/web-app-suitecrm/README.md new file mode 100644 index 00000000..c1abed63 --- /dev/null +++ b/roles/web-app-suitecrm/README.md @@ -0,0 +1,48 @@ +# SuiteCRM + +## Description + +Manage your customer relationships with SuiteCRM, a powerful open-source CRM platform extending SugarCRM with advanced modules, workflows, and integrations. This role integrates SuiteCRM into the Infinito.Nexus ecosystem with centralized database, mail, LDAP and OIDC-ready SSO support. πŸš€πŸ’Ό + +## Overview + +This Ansible role deploys SuiteCRM using Docker and the Infinito.Nexus shared stack. It handles: + +- MariaDB database provisioning via the `sys-svc-rdbms` role +- Nginx domain and reverse-proxy configuration +- Environment variable management through Jinja2 templates +- Docker Compose orchestration for the **SuiteCRM** application container +- Native **LDAP** authentication via Symfony’s LDAP configuration +- OIDC-ready wiring for integration with Keycloak or other OIDC providers (via reverse proxy or plugin) + +With this role, you get a production-ready CRM environment that plugs into your existing IAM stack. + +## Features + +- **Sales & Service CRM:** Accounts, Contacts, Leads, Opportunities, Cases, Campaigns and more πŸ“Š +- **Workflow Engine:** Automate business processes and notifications πŸ› οΈ +- **LDAP Authentication:** Centralize user authentication against OpenLDAP πŸ” +- **OIDC-Ready SSO:** Preconfigured OIDC environment variables for use with plugins or an OIDC reverse proxy 🌐 +- **Config via Templates:** Fully customizable `.env` and `docker-compose.yml` rendered via Jinja2 βš™οΈ +- **Health Checks & Logging:** Integrates with Infinito.Nexus health checking and journald logging πŸ“ˆ +- **Modular Role Composition:** Uses shared roles for DB, proxy and monitoring to keep your stack consistent πŸ”„ + +## Further Resources + +- [SuiteCRM Official Website](https://suitecrm.com/) 🌍 +- [SuiteCRM Documentation](https://docs.suitecrm.com/) πŸ“– +- [Infinito.Nexus Project Repository](https://s.infinito.nexus/code) πŸ”— + +## OIDC & LDAP Notes + +- **LDAP** is configured using Symfony’s environment variables (`AUTH_TYPE=ldap`, `LDAP_*`) so SuiteCRM 8+ can authenticate directly against your OpenLDAP service. +- **OIDC** is provided at the platform level (e.g. Keycloak + oauth2-proxy or a SuiteCRM OIDC plugin). + This role exposes OIDC client, issuer and endpoint settings as environment variables, so plugins or + sidecar components can consume them without duplicating configuration. + +## Credits + +Developed and maintained by **Kevin Veen-Birkenbach**. +Consulting & Coaching Solutions: [veen.world](https://www.veen.world) 🌟 +Part of the [Infinito.Nexus Project](https://s.infinito.nexus/code) πŸ“‚ +License: [Infinito.Nexus NonCommercial License](https://s.infinito.nexus/license) βš–οΈ diff --git a/roles/web-app-suitecrm/config/main.yml b/roles/web-app-suitecrm/config/main.yml new file mode 100644 index 00000000..b0eac509 --- /dev/null +++ b/roles/web-app-suitecrm/config/main.yml @@ -0,0 +1,40 @@ +features: + matomo: true + css: true + desktop: true + ldap: true + oidc: true # OIDC via Keycloak + reverse proxy / plugin + central_database: true + logout: true + +server: + csp: + flags: {} + whitelist: + # Allow data URIs for icons etc. + connect-src: + - "data:" + domains: + aliases: [] + canonical: + - suite.crm.{{ PRIMARY_DOMAIN }} + +email: + from_name: "Customer Relationship Management (SuiteCRM, {{ PRIMARY_DOMAIN }})" + +docker: + services: + database: + enabled: true + suitecrm: + image: "bitnami/suitecrm" + version: "8" + name: "suitecrm" + cpus: 1.5 + mem_reservation: 1.2g + mem_limit: 2g + pids_limit: 768 + volumes: + data: suitecrm_data + +maintenance_mode: false diff --git a/roles/web-app-suitecrm/meta/main.yml b/roles/web-app-suitecrm/meta/main.yml new file mode 100644 index 00000000..f14b586c --- /dev/null +++ b/roles/web-app-suitecrm/meta/main.yml @@ -0,0 +1,23 @@ +galaxy_info: + author: "Kevin Veen-Birkenbach" + description: > + Power your sales and service processes with SuiteCRM. + license: "Infinito.Nexus NonCommercial License" + license_url: "https://s.infinito.nexus/license" + company: | + Kevin Veen-Birkenbach + Consulting & Coaching Solutions + https://www.veen.world + galaxy_tags: + - suitecrm + - crm + - sales + repository: "https://s.infinito.nexus/code" + issue_tracker_url: "https://s.infinito.nexus/issues" + documentation: "https://docs.infinito.nexus" + logo: + class: "fa-solid fa-phone" + run_after: + - web-app-keycloak + - web-app-mailu + - web-app-mastodon diff --git a/roles/web-app-suitecrm/schema/main.yml b/roles/web-app-suitecrm/schema/main.yml new file mode 100644 index 00000000..e69de29b diff --git a/roles/web-app-suitecrm/tasks/main.yml b/roles/web-app-suitecrm/tasks/main.yml new file mode 100644 index 00000000..2b422fb7 --- /dev/null +++ b/roles/web-app-suitecrm/tasks/main.yml @@ -0,0 +1,6 @@ +--- +- name: "Load docker, db and proxy for {{ application_id }}" + include_role: + name: sys-stk-full-stateful + vars: + docker_compose_flush_handlers: true diff --git a/roles/web-app-suitecrm/templates/Dockerfile.j2 b/roles/web-app-suitecrm/templates/Dockerfile.j2 new file mode 100644 index 00000000..8337c315 --- /dev/null +++ b/roles/web-app-suitecrm/templates/Dockerfile.j2 @@ -0,0 +1 @@ +FROM "{{ SUITECRM_IMAGE }}:{{ SUITECRM_VERSION }}" diff --git a/roles/web-app-suitecrm/templates/docker-compose.yml.j2 b/roles/web-app-suitecrm/templates/docker-compose.yml.j2 new file mode 100644 index 00000000..ef0ab81f --- /dev/null +++ b/roles/web-app-suitecrm/templates/docker-compose.yml.j2 @@ -0,0 +1,24 @@ +{% include 'roles/docker-compose/templates/base.yml.j2' %} + +{% set service_name = SUITECRM_SERVICE %} + {{ service_name }}: + {{ lookup('template', 'roles/docker-container/templates/build.yml.j2') | indent(4) }} + container_name: {{ SUITECRM_CONTAINER }} + image: "{{ SUITECRM_CUSTOM_IMAGE }}" + init: true + stop_signal: SIGTERM + stop_grace_period: 30s +{% include 'roles/docker-container/templates/base.yml.j2' %} +{% include 'roles/docker-container/templates/healthcheck/curl.yml.j2' %} + ports: + - "127.0.0.1:{{ ports.localhost.http[application_id] }}:80" +{% include 'roles/docker-container/templates/depends_on/dmbs_excl.yml.j2' %} +{% include 'roles/docker-container/templates/networks.yml.j2' %} + volumes: + - data:/bitnami/suitecrm + +{% include 'roles/docker-compose/templates/volumes.yml.j2' %} + data: + name: {{ SUITECRM_DATA_VOLUME }} + +{% include 'roles/docker-compose/templates/networks.yml.j2' %} diff --git a/roles/web-app-suitecrm/templates/env.j2 b/roles/web-app-suitecrm/templates/env.j2 new file mode 100644 index 00000000..c6b792ac --- /dev/null +++ b/roles/web-app-suitecrm/templates/env.j2 @@ -0,0 +1,83 @@ +############################################# +# SuiteCRM Docker Environment (.env) – ENGLISH +# Based on bitnami/suitecrm environment variables +############################################# + +# ------------------------------------------------ +# Database connection (Bitnami-style) +# ------------------------------------------------ +MARIADB_HOST={{ database_host }} +MARIADB_PORT={{ database_port }} +MARIADB_USER={{ database_username }} +MARIADB_PASSWORD={{ database_password }} + +# ------------------------------------------------ +# Initial admin account +# ------------------------------------------------ +SUITECRM_USERNAME={{ applications | get_app_conf(application_id, 'users.administrator.username') }} +SUITECRM_PASSWORD={{ applications | get_app_conf(application_id, 'credentials.administrator_password') }} +SUITECRM_EMAIL={{ users['contact'].email }} +SUITECRM_HOST={{ domains | get_domain(application_id) }} + +# ------------------------------------------------ +# Logger / basic options +# (SuiteCRM/Bitnami will log to stdout/stderr by default) +# ------------------------------------------------ + +# ------------------------------------------------ +# System SMTP settings +# ------------------------------------------------ +SUITECRM_SMTP_HOST={{ SYSTEM_EMAIL.HOST }} +SUITECRM_SMTP_PORT={{ SYSTEM_EMAIL.PORT }} +SUITECRM_SMTP_USER={{ users['contact'].email }} +SUITECRM_SMTP_PASSWORD={{ users['contact'].mailu_token }} +SUITECRM_SMTP_PROTOCOL={{ "TLS" if SYSTEM_EMAIL.START_TLS else "SSL" }} + +# ------------------------------------------------ +# LDAP settings (native SuiteCRM 8+ via Symfony) +# Applied only if the feature flag is true +# ------------------------------------------------ +{% if applications | get_app_conf(application_id, 'features.ldap') %} +AUTH_TYPE=ldap + +# Base LDAP connection +LDAP_HOST={{ LDAP.SERVER.DOMAIN }} +LDAP_PORT={{ LDAP.SERVER.PORT }} +# Allowed values: none, tls, ssl (see SuiteCRM docs) +LDAP_ENCRYPTION={{ LDAP.SERVER.SECURITY | lower if LDAP.SERVER.SECURITY else "none" }} +LDAP_PROTOCOL_VERSION=3 +LDAP_REFERRALS=false + +# DN / search configuration +LDAP_DN_STRING='{{ LDAP.USER.ATTRIBUTES.ID }}={username},{{ LDAP.DN.OU.USERS }}' +LDAP_QUERY_STRING='' +LDAP_SEARCH_DN={{ LDAP.DN.ADMINISTRATOR.DATA }} +LDAP_SEARCH_PASSWORD={{ LDAP.BIND_CREDENTIAL }} + +# Auto-create SuiteCRM users from LDAP +LDAP_AUTO_CREATE=enabled +LDAP_PROVIDER_BASE_DN={{ LDAP.DN.OU.USERS }} +LDAP_PROVIDER_SEARCH_DN={{ LDAP.DN.ADMINISTRATOR.DATA }} +LDAP_PROVIDER_SEARCH_PASSWORD={{ LDAP.BIND_CREDENTIAL }} +LDAP_PROVIDER_DEFAULT_ROLES=ROLE_USER +LDAP_PROVIDER_UID_KEY={{ LDAP.USER.ATTRIBUTES.ID }} +LDAP_PROVIDER_FILTER='({{ LDAP.USER.ATTRIBUTES.ID }}={username})' +{% endif %} + +# ------------------------------------------------ +# OpenID Connect settings (platform / plugin level) +# Applied only if the feature flag is true +# ------------------------------------------------ +{% if SUITECRM_OIDC_ENABLED | bool %} +SUITECRM_OIDC_CLIENT_ID={{ OIDC.CLIENT.ID }} +SUITECRM_OIDC_CLIENT_SECRET={{ OIDC.CLIENT.SECRET }} + +SUITECRM_OIDC_ISSUER_URL={{ OIDC.CLIENT.ISSUER_URL }} +SUITECRM_OIDC_AUTHORIZATION_ENDPOINT={{ OIDC.CLIENT.AUTHORIZE_URL }} +SUITECRM_OIDC_TOKEN_ENDPOINT={{ OIDC.CLIENT.TOKEN_URL }} +SUITECRM_OIDC_USER_INFO_ENDPOINT={{ OIDC.CLIENT.USER_INFO_URL }} +SUITECRM_OIDC_JWKS_ENDPOINT={{ OIDC.CLIENT.CERTS }} + +SUITECRM_OIDC_REDIRECT_URI={{ SUITECRM_URL }}/oidc/callback +SUITECRM_OIDC_SCOPES=openid,profile,email +{% endif %} diff --git a/roles/web-app-suitecrm/users/main.yml b/roles/web-app-suitecrm/users/main.yml new file mode 100644 index 00000000..2afaf023 --- /dev/null +++ b/roles/web-app-suitecrm/users/main.yml @@ -0,0 +1,7 @@ +users: + administrator: + username: "administrator" + contact: + username: "contact" + roles: + - mail-bot diff --git a/roles/web-app-suitecrm/vars/main.yml b/roles/web-app-suitecrm/vars/main.yml new file mode 100644 index 00000000..20b773ab --- /dev/null +++ b/roles/web-app-suitecrm/vars/main.yml @@ -0,0 +1,24 @@ +# General +application_id: "web-app-suitecrm" +entity_name: "{{ application_id | get_entity_name }}" + +# Database +database_type: "mariadb" + +# Webserver +client_max_body_size: "100m" +vhost_flavour: "generic" + +# SuiteCRM container settings +SUITECRM_VERSION: "{{ applications | get_app_conf(application_id, 'docker.services.' ~ entity_name ~ '.version') }}" +SUITECRM_IMAGE: "{{ applications | get_app_conf(application_id, 'docker.services.' ~ entity_name ~ '.image') }}" +SUITECRM_CUSTOM_IMAGE: "custom_suitecrm" +SUITECRM_CONTAINER: "{{ applications | get_app_conf(application_id, 'docker.services.' ~ entity_name ~ '.name') }}" +SUITECRM_SERVICE: "{{ entity_name }}" + +# Volumes +SUITECRM_DATA_VOLUME: "{{ applications | get_app_conf(application_id, 'docker.volumes.data') }}" + +# URLs & features +SUITECRM_URL: "{{ domains | get_url(application_id, WEB_PROTOCOL) }}" +SUITECRM_OIDC_ENABLED: "{{ applications | get_app_conf(application_id, 'features.oidc') }}"