fix(web-app-suitecrm): keep sessions in Redis so replicas share them

With three swarm replicas the PHP session files on the shared NFS volume
were read as empty under concurrent requests of one browser, so SuiteCRM
answered 403 on /api/graphql and dropped the session. Sessions now live in
Redis with session locking that waits like flock did; the phpredis version
is pinned in services.yml with its own update source, and the engine lookup
renders the save path.

Refs #879

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
kevinveenbirkenbachandClaude Opus 5.5 committed 2026-10-08 18:31:19 +02:00
1 parent 5c862ee53d
commit 3cefb01d71
9 files changed
+120 -7

No files matched your search

@@ -0,0 +1,49 @@
"""phpredis reads ``auth[user]``/``auth[pass]`` and ``prefix`` from the query of
``session.save_path``; a password with a reserved character that is not
percent-encoded splits the query and the handler authenticates with a fragment.
"""
from __future__ import annotations
import importlib
import unittest
from urllib.parse import parse_qs, urlsplit
plugin_module = importlib.import_module("plugins.lookup.engine")
redis_session_path = plugin_module.redis_session_path
class TestRedisSessionPath(unittest.TestCase):
def test_an_embedded_sidecar_gets_no_credentials(self) -> None:
self.assertEqual(
"tcp://redis:6379?prefix=suitecrm:session:",
redis_session_path("redis", 6379, "suitecrm", "", ""),
)
def test_a_shared_redis_gets_user_and_password(self) -> None:
query = parse_qs(
urlsplit(
redis_session_path("redis-central", 6379, "suitecrm", "default", "pw")
).query
)
self.assertEqual(["default"], query["auth[user]"])
self.assertEqual(["pw"], query["auth[pass]"])
def test_a_password_with_reserved_characters_survives_the_query(self) -> None:
password = "a&b=c?d#e/f+g h"
query = parse_qs(
urlsplit(
redis_session_path("redis-central", 6379, "suitecrm", "default", password)
).query
)
self.assertEqual([password], query["auth[pass]"])
self.assertEqual(["suitecrm:session:"], query["prefix"])
def test_a_user_without_a_password_is_not_sent(self) -> None:
self.assertNotIn(
"auth", redis_session_path("redis", 6379, "suitecrm", "default", "")
)
if __name__ == "__main__":
unittest.main()