mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-06 10:59:59 +00:00
495d23dbderaised an admin context with a fresh in-memory bean, which fixed hasSaveAccess() and moved the failure one guard further. User::verify_data() compares $this->id == $current_user->id, and between two unsaved beans that is '' == '', so a deployment with a single administrator takes the last-admin branch for the very account CreateUser is in the middle of creating. The branch reads the legacy $mod_strings, which this request never loads. That is why the two SuiteCRM jobs of CI run 38818071194 looked like two failures: the LDAP variant ended in ErrorException "Trying to access array offset on value of type null" at modules/Users/User.php:1678 and answered HTTP 500 to POST /login, while the SAML variant took the same verify_data() failure, avoided the 500 because $mod_strings happened to be loaded, redirected to index.php?action=Error&module=Users and left no session, so its spec timed out waiting for the SPA instead. Both are the first login of a persona that has no local row yet; administrator passes in both because it already has one. Use the stored system user, which has a real id, so hasSaveAccess() passes, restrictAdminOnlyFields() early-returns, setIsAdmin()'s reloaded bean resolves and '' == '1' skips the last-admin branch. Assign $current_user before calling getSystemUser(), not from its return value. User::retrieve() runs loadPreferences() after filling the bean, and both UserPreference::loadPreferences() and reloadPreferences() dereference $current_user->id unguarded, so retrieving while the caller's anonymous $current_user is still in place raises "Attempt to read property id on null" and answers HTTP 500 one line further on. Assigning first also means the only session preferences key written is the system user's own, never the key of the account signing in. The patch header records both failure modes, because the patch outlives this commit and is applied at image build time with -F0, so whoever reads it next when a hunk stops matching needs to know what it was for. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> (cherry picked from commit207cdc70ff)
162 lines
8.2 KiB
Markdown
162 lines
8.2 KiB
Markdown
# 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 and LDAP-ready single sign-on integration. 🚀💼
|
||
|
||
## 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
|
||
- Declarative **SAML** SSO against Keycloak (`AUTH_TYPE=saml`, wired entirely from env vars — no admin-panel step)
|
||
|
||
With this role, you get a production-ready CRM environment that plugs into your existing IAM stack.
|
||
|
||
## Cosmos
|
||
|
||
The diagram places SuiteCRM in the Infinito.Nexus cosmos: the components it deploys (capabilities), the central services it consumes (dependencies), and its outward reach (federation and bridged external networks).
|
||
|
||
```mermaid
|
||
flowchart LR
|
||
subgraph deps [Dependencies]
|
||
dep_svc_bkp_volume_2_local["svc-bkp-volume-2-local 💻"]
|
||
dep_svc_db_mariadb["svc-db-mariadb 🐳🐝"]
|
||
dep_svc_db_openldap["svc-db-openldap 🐳🐝"]
|
||
dep_svc_db_redis["svc-db-redis 🐳🐝"]
|
||
dep_svc_net_tor["svc-net-tor 🐳🐝"]
|
||
dep_web_app_dashboard["web-app-dashboard 🐳🐝"]
|
||
dep_web_app_keycloak["web-app-keycloak 🐳🐝"]
|
||
dep_web_app_matomo["web-app-matomo 🐳🐝"]
|
||
dep_web_app_prometheus["web-app-prometheus 🐳🐝"]
|
||
dep_web_app_stalwart["web-app-stalwart 🐳🐝"]
|
||
dep_web_svc_css["web-svc-css 💻"]
|
||
dep_web_svc_logout["web-svc-logout 🐳🐝"]
|
||
end
|
||
subgraph role [web-app-suitecrm 🐳🐝]
|
||
svc_sso["sso"]
|
||
svc_logout["logout"]
|
||
svc_ldap["ldap"]
|
||
svc_dashboard["dashboard"]
|
||
svc_matomo["matomo"]
|
||
svc_email["email"]
|
||
svc_mariadb["mariadb"]
|
||
svc_redis["redis"]
|
||
svc_suitecrm["suitecrm"]
|
||
svc_css["css"]
|
||
svc_prometheus["prometheus"]
|
||
svc_tor["tor"]
|
||
svc_container_backup["container_backup"]
|
||
end
|
||
dep_svc_bkp_volume_2_local -. "0..1" .-> svc_container_backup
|
||
dep_svc_db_mariadb -. "0..1" .-> svc_mariadb
|
||
dep_svc_db_openldap -. "0..1" .-> svc_ldap
|
||
dep_svc_db_redis -. "0..1" .-> svc_redis
|
||
dep_svc_net_tor -. "0..1" .-> svc_tor
|
||
dep_web_app_dashboard -. "0..1" .-> svc_dashboard
|
||
dep_web_app_keycloak -. "0..1" .-> svc_sso
|
||
dep_web_app_matomo -. "0..1" .-> svc_matomo
|
||
dep_web_app_prometheus -. "0..1" .-> svc_prometheus
|
||
dep_web_app_stalwart -. "0..1" .-> svc_email
|
||
dep_web_svc_css -. "0..1" .-> svc_css
|
||
dep_web_svc_logout -. "0..1" .-> svc_logout
|
||
```
|
||
|
||
Solid `1:1` edges are fixed relationships; dashed `0..1` edges are conditional (enabled only in matching deployments); red `0..0` edges are turned off in this role. Node markers show the role's deploy modes (💻 host, 🐳 compose, 🐝 swarm); ❌ marks a service that is explicitly turned off, and ⚙️ an Ansible role dependency declared in `meta/main.yml`.
|
||
|
||
## 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 🔐
|
||
- **SSO-Ready:** SuiteCRM's own SAML login against Keycloak, configured declaratively from the rendered env 🌐
|
||
- **Config via Templates:** Fully customizable `.env` and `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 🔄
|
||
|
||
## Quick Setup
|
||
|
||
### Development
|
||
|
||
Clone, set up the workstation, and deploy SuiteCRM onto the local stack:
|
||
|
||
```bash
|
||
git clone https://github.com/infinito-nexus/core.git
|
||
cd core
|
||
make onboard
|
||
make compose-deploy mode=reinstall apps=web-app-suitecrm full_cycle=false
|
||
```
|
||
|
||
### Production
|
||
|
||
Run the published image to provision the inventory and deploy SuiteCRM to a managed server (the mounted volume persists the inventory):
|
||
|
||
```bash
|
||
APP=web-app-suitecrm
|
||
HOST=<your-server>
|
||
DOMAIN=<your-domain>
|
||
TLS_MODE=self_signed
|
||
SSH_PUBLIC_KEY="<your-ssh-public-key>"
|
||
|
||
docker run --rm -it \
|
||
-v "$PWD/inventories:/etc/infinito.nexus/inventories" \
|
||
-e APP="$APP" -e HOST="$HOST" -e DOMAIN="$DOMAIN" -e TLS_MODE="$TLS_MODE" -e SSH_PUBLIC_KEY="$SSH_PUBLIC_KEY" \
|
||
ghcr.io/infinito-nexus/core/debian bash -c '
|
||
INVENTORY=/etc/infinito.nexus/inventories/production
|
||
infinito administration inventory provision "$INVENTORY" \
|
||
--inventory-file "$INVENTORY/devices.yml" \
|
||
--host "$HOST" \
|
||
--include "$APP" \
|
||
--vars "{\"TLS_MODE\": \"$TLS_MODE\", \"DOMAIN_PRIMARY\": \"$DOMAIN\", \"users\": {\"administrator\": {\"authorized_keys\": [\"$SSH_PUBLIC_KEY\"]}}}" &&
|
||
infinito administration deploy dedicated "$INVENTORY/devices.yml" \
|
||
--password-file "$INVENTORY/.password" \
|
||
--diff -vv'
|
||
```
|
||
|
||
## Further Resources
|
||
|
||
- [SuiteCRM Official Website](https://suitecrm.com/) 🌍
|
||
- [SuiteCRM Documentation](https://docs.suitecrm.com/) 📖
|
||
- [Infinito.Nexus Project Repository](https://s.infinito.nexus/code) 🔗
|
||
|
||
## LDAP & SSO Notes
|
||
|
||
- **LDAP** is configured via environment variables (`AUTH_TYPE=ldap`, `LDAP_*`) in
|
||
[`templates/env.j2`](./templates/env.j2). The role additionally renders
|
||
[`templates/ldap.yaml.j2`](./templates/ldap.yaml.j2) into the container's
|
||
`extensions/<software>/config/services/ldap/`, which maps the LDAP attributes onto
|
||
SuiteCRM's user fields for auto-created accounts.
|
||
|
||
- **SSO** is SuiteCRM's own SAML login against Keycloak (`sso.flavor: saml` in
|
||
[`meta/services.yml`](./meta/services.yml)), not a proxy in front of it. Unauthenticated
|
||
requests hit the Symfony firewall's SAML entry point, which redirects to Keycloak;
|
||
the assertion comes back to `/saml/acs` and establishes a real SuiteCRM session, so the
|
||
app renders its own authenticated UI including its logout control. Accounts are created
|
||
on first login from the assertion (`SAML_AUTO_CREATE`), and the IdP signing certificate
|
||
is read from Keycloak's realm descriptor by
|
||
[`files/shell/docker-entrypoint.sh`](./files/shell/docker-entrypoint.sh) at boot, over
|
||
the Tor SOCKS proxy whenever the issuer is an onion, because a `.onion` issuer is
|
||
unresolvable directly. `/auth` stays reachable as the local break-glass login.
|
||
|
||
- **Auto-created accounts** reach `User::save()` through SuiteCRM's own `CreateUser`
|
||
while the request is still anonymous. Since 8.10.2 that save answers
|
||
`RuntimeException: Not authorized` and forces `status = Inactive`, so
|
||
`files/patch/external-auth-user-create.patch` makes the stored system user the current
|
||
user around the save and restores the caller's afterwards.
|
||
[`files/Dockerfile`](./files/Dockerfile) applies it with `patch -F0` on the unpacked
|
||
release; the image build fails once the hunk stops matching.
|
||
|
||
## Persona contract opt-outs
|
||
|
||
[`templates/env.j2`](./templates/env.j2) seeds only the administrator into SuiteCRM (`SUITECRM_ADMIN_USERNAME` / `SUITECRM_ADMIN_PASSWORD`); `biber` reaches the app through SuiteCRM's own SAML login against Keycloak, where `SAML_AUTO_CREATE` mints the account on first assertion. In the `services.sso.enabled: false` matrix variants there is no SAML entry point and `biber` has no native account, so [`templates/playwright.env.j2`](./templates/playwright.env.j2) renders `PERSONA_BIBER_BLOCKED=true` for the generic persona journeys (the LDAP spec still signs `biber` in from the directory). The `administrator` persona keeps its native login via `ADMIN_NATIVE_PASSWORD` and runs in every variant.
|
||
|
||
## Credits
|
||
|
||
Implemented by **[Kevin Veen-Birkenbach](https://www.veen.world)**.
|
||
Part of the [Infinito.Nexus Project](https://s.infinito.nexus/code) and maintained by [Kevin Veen-Birkenbach](https://www.veen.world).
|
||
Licensed under the [Infinito.Nexus Community License (Non-Commercial)](https://s.infinito.nexus/license).
|