Files
computer-playbook/roles/web-app-suitecrm/files/shell/docker-entrypoint.sh
kevinveenbirkenbachandClaude Opus 5.5 ab9c23fa76 fix(web-app-suitecrm): run Symfony's session through the Redis handler too
3cefb01d71 moved PHP sessions to Redis by pointing session.save_path at
redis-central. SuiteCRM's own core_services.yaml wraps a
NativeFileSessionHandler around '%session.save_path%', and that handler
forces session.save_handler back to files and treats the Redis URL as a
directory ("is_dir(): Unable to find the wrapper "tcp""). Symfony's session
was empty on every request, so every POST /api/graphql answered 403
"Invalid CSRF token", the SPA logged the user out right after the SAML login
and the biber logout spec failed with "session was lost before the logout
attempt" (SuiteCRM#0 on PR #869, reproduced on main locally).

core_services.yaml is imported after every extension service file, so the
core handler cannot be redefined there. An extension packages file now
points framework.session at our own handler and storage factory, which wrap
PHP's built-in SessionHandler and therefore use phpredis from php.ini, with
its locking, for Symfony and the legacy app alike.

The entrypoint rebuilds the prod cache when an extension config is newer
than it, so an existing installation picks the wiring up instead of keeping
the compiled container.

Verified with a local compose deploy of variant 0: on main the SAML logout
spec failed with the 403 sequence above; after a mode=update redeploy the
cache was rebuilt, all three SAML specs passed, and the prod log shows no
tcp-wrapper warning or CSRF failure after the rebuild.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-12 18:35:22 +02:00

147 lines
5.8 KiB
Bash

#!/bin/sh
set -eu
APP_DIR="${SUITECRM_APP_DIR:?SUITECRM_APP_DIR must be baked into the image}"
WEB_USER="${SUITECRM_WEB_USER:?SUITECRM_WEB_USER must be baked into the image}"
WEB_GROUP="${SUITECRM_WEB_GROUP:?SUITECRM_WEB_GROUP must be baked into the image}"
INSTALL_FLAG="${APP_DIR}/public/installed.flag"
SEED_DIR="${SUITECRM_SEED_DIR:?SUITECRM_SEED_DIR must be baked into the image}"
APP_VERSION="${SUITECRM_APP_VERSION:?SUITECRM_APP_VERSION must be baked into the image}"
SEED_STAMP="${APP_DIR}/.tree-seeded"
log() { printf '%s %s\n' "[suitecrm-entrypoint]" "$*" >&2; }
if [ ! -d "$APP_DIR" ]; then
log "ERROR: Application directory '$APP_DIR' does not exist."
exit 1
fi
if [ -n "${SUITECRM_SAML_IDP_DESCRIPTOR_URL:-}" ]; then
log "Fetching the IdP signing certificate from ${SUITECRM_SAML_IDP_DESCRIPTOR_URL}..."
_descriptor=$(curl -fsS --connect-timeout 20 --max-time 25 --retry 20 --retry-all-errors --retry-delay 5 --proxy "${SUITECRM_OIDC_SOCKS_PROXY:-}" "$SUITECRM_SAML_IDP_DESCRIPTOR_URL")
_tags=$(printf '%s' "$_descriptor" | tr '<' '\n')
SAML_IDP_X509CERT=$(printf '%s' "$_tags" | awk -F'>' '/KeyDescriptor[^>]*use="signing"/ { s=1 } s && $1 ~ /X509Certificate$/ && $2 != "" { print $2; exit }')
if [ -z "$SAML_IDP_X509CERT" ]; then
SAML_IDP_X509CERT=$(printf '%s' "$_tags" | awk -F'>' '$1 ~ /X509Certificate$/ && $2 != "" { print $2; exit }')
fi
if [ -z "$SAML_IDP_X509CERT" ]; then
log "ERROR: no X509Certificate in the IdP descriptor."
exit 1
fi
export SAML_IDP_X509CERT
log "IdP certificate loaded."
fi
TMPDIR="${APP_DIR}/tmp"
export TMPDIR
mkdir -p "$TMPDIR"
chown "$WEB_USER:$WEB_GROUP" "$TMPDIR"
chmod 775 "$TMPDIR"
BOOT_LOCK="${APP_DIR}/.suitecrm-boot.lock.d"
_have_lock=0
_lock_tries=0
while :; do
if mkdir "$BOOT_LOCK" 2>/dev/null; then
_have_lock=1
break
fi
# Exception: 1800s exceeds the orchestrator kill ceiling (start_period 20m
# plus retries), so only a dead leader's lock can be this old.
_lock_mtime=$(stat -c %Y "$BOOT_LOCK" 2>/dev/null || echo 0)
if [ "$_lock_mtime" -gt 0 ] && [ $(($(date +%s) - _lock_mtime)) -ge 1800 ]; then
log "Stale boot lock detected - removing it and retrying."
rmdir "$BOOT_LOCK" 2>/dev/null || true # nocheck: shell-or-true -- grandfathered: worked in practice; TODO: sharpen to catch only the exact tolerated error
fi
if [ -f "$INSTALL_FLAG" ]; then
break
fi
_lock_tries=$((_lock_tries + 1))
if [ "$_lock_tries" -ge 420 ]; then
log "ERROR: timed out waiting for the boot lock or install flag."
exit 1
fi
sleep 5
done
if [ "$_have_lock" = "1" ]; then
trap 'rmdir "$BOOT_LOCK" 2>/dev/null || true' EXIT
trap 'rmdir "$BOOT_LOCK" 2>/dev/null || true; exit 143' TERM INT # nocheck: shell-or-true -- grandfathered: worked in practice; TODO: sharpen to catch only the exact tolerated error
if [ ! -f "$SEED_STAMP" ]; then
if [ -f "$INSTALL_FLAG" ]; then
log "Installed volume without seed stamp - marking as seeded, keeping the live tree."
else
log "Seeding SuiteCRM tree from ${SEED_DIR} into ${APP_DIR}..."
cp -a "${SEED_DIR}/." "${APP_DIR}/"
log "Seed complete."
fi
echo "$APP_VERSION" > "$SEED_STAMP"
chown "$WEB_USER:$WEB_GROUP" "$SEED_STAMP"
else
_seeded_version=$(cat "$SEED_STAMP")
if [ "$_seeded_version" = "seeded" ]; then
echo "$APP_VERSION" > "$SEED_STAMP"
elif [ "$_seeded_version" != "$APP_VERSION" ]; then
log "WARNING: volume tree is ${_seeded_version}, image ships ${APP_VERSION} - the tree is NOT re-seeded, upgrade it manually."
fi
fi
CACHE_REFRESH=0
if [ ! -f "$INSTALL_FLAG" ]; then
CACHE_REFRESH=1
log "SuiteCRM 8 is not installed - performing automated installation..."
php bin/console suitecrm:app:install \
-u "$SUITECRM_ADMIN_USERNAME" \
-p "$SUITECRM_ADMIN_PASSWORD" \
-U "$SUITECRM_DB_USER" \
-P "$SUITECRM_DB_PASSWORD" \
-H "$SUITECRM_DB_HOST" \
-Z "$SUITECRM_DB_PORT" \
-N "$SUITECRM_DB_NAME" \
-S "$SUITECRM_URL" \
-d "no"
echo "installed" > "$INSTALL_FLAG"
chown "$WEB_USER:$WEB_GROUP" "$INSTALL_FLAG"
log "SuiteCRM installation completed successfully."
else
log "SuiteCRM already installed - skipping installer."
fi
if [ -d "${APP_DIR}/cache/prod" ] && [ -n "$(find "${APP_DIR}/extensions" -path '*/config/*' -type f -newer "${APP_DIR}/cache/prod" -print -quit)" ]; then
CACHE_REFRESH=1
log "Extension config is newer than the prod cache - refreshing it."
fi
if [ "$CACHE_REFRESH" = "1" ] || [ ! -d "${APP_DIR}/cache/prod" ]; then
log "Clearing Symfony cache..."
php bin/console cache:clear --no-warmup || true # nocheck: shell-or-true -- grandfathered: worked in practice; TODO: sharpen to catch only the exact tolerated error
php bin/console cache:warmup || true # nocheck: shell-or-true -- grandfathered: worked in practice; TODO: sharpen to catch only the exact tolerated error
else
log "Existing prod cache - skipping cache:clear/warmup."
fi
if [ "$CACHE_REFRESH" = "1" ]; then
chown -R "$WEB_USER:$WEB_GROUP" "${APP_DIR}/public/legacy"
fi
# Exception: install/cache:clear above run as root; the legacy language
# caches they wipe are regenerated lazily by apache as www-data, which
# cannot write into root-owned cache dirs -> permanent 500 without this.
chown -R "$WEB_USER:$WEB_GROUP" "${APP_DIR}/cache" "${APP_DIR}/public/legacy/cache" 2>/dev/null || true # nocheck: shell-or-true -- grandfathered: worked in practice; TODO: sharpen to catch only the exact tolerated error
chown -R "$WEB_USER:$WEB_GROUP" "${APP_DIR}/logs"
rmdir "$BOOT_LOCK" 2>/dev/null || true # nocheck: shell-or-true -- grandfathered: worked in practice; TODO: sharpen to catch only the exact tolerated error
trap - EXIT TERM INT
else
log "SuiteCRM installed by another replica - skipping installer and cache pass."
fi
log "Starting $*..."
exec "$@"