SuiteCRM 8.10.2 tightened User::hasSaveAccess() from return true to is_admin($current_user) for a record without an id, and restrictAdminOnlyFields() now forces status Inactive on such a record. CreateUser::create() runs while the browser is still anonymous, because it is the LDAP and SAML auto-create path, so every FIRST login of a directory account ends in RuntimeException: Not authorized and HTTP 500 on POST /login. The pre-existing administrator is unaffected, which is why one persona passes and one fails in both auth modes. 451f63cbdd carried the role from 8.9.1 to 8.10.2; 8.9.1 exempted a new record from both guards.
The patch raises the admin context around that one save and restores it in a finally, rather than relaxing the guards, which stay closed for every request-driven save. create() has a single caller in its own file, createExternalAuthUser, which sets is_admin 0 and external_auth_only 1 before calling, so the raised context cannot mint an administrator. patch runs with -F0 so a hunk upstream has moved fails the build instead of landing somewhere else.
The patch path is derived once in vars/main.yml and reaches the Dockerfile as a build arg, following the role's existing SUITECRM_ENTRYPOINT_SCRIPT_* shape, so the file name is written once.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 495d23dbde)
# Conflicts:
# roles/web-app-suitecrm/vars/main.yml
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>
The entrypoint runs the installer and the cache commands as root. They
create logs/prod, and the entrypoint handed cache and public/legacy back
to the web user but not logs. Apache could not write there, so SuiteCRM
wrote neither its auth.log nor its main log.
The entrypoint now hands logs to the web user after the root-run console
commands, on every boot that holds the boot lock, so volumes of earlier
deploys are repaired too. The deploy fails if a path under logs is not
writable for the web user.
Closes#854
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Every swarm task of the tor variant died 1.4s after start with exit 8, so the
stack never converged and the drill failed at sys-svc-compose's converge wait
(CI run 32119014832, job 95662863105). The task log held exactly one line: the
entrypoint's "Fetching the IdP signing certificate" - nothing after it.
GNU wget cannot parse a socks5h:// proxy. It aborts before sending anything
with "Error parsing proxy URL: Unsupported scheme.", which src/retr.c maps to
PROXERR and src/exits.c to WGET_EXIT_SERVER_ERROR, i.e. 8 - identical in 1.21
through 1.25. Under -qO- the message is suppressed, which is why the container
log ends mid-sentence. Reproduced locally: the same URL without a proxy exits
4, with the socks5h proxy exits 8.
The proxy was injected container-wide as http_proxy/https_proxy, gated on
Keycloak's domain but applied to every destination - and the CI Tor network
has no exit nodes, so that also dead-ended every clearnet call the app makes.
The three sibling roles that solved this (wordpress, nextcloud, joomla) each
gate per destination host instead. Here the only server-side onion call is the
descriptor fetch itself: the SAML bundle is configured statically, so it needs
no backchannel, and the healthcheck already forces --noproxy '*'. The proxy
now reaches exactly the one client that needs it.
curl's timeouts are pinned so a stalled onion cannot hang the boot: 21
attempts of at most 30s bound the fetch at ~10.5min, inside the 20m
start_period and the ~28min converge budget.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
meta/volumes.yml mounts ldap.yaml only when services.ldap.enabled, and
compose_volumes drops the whole configs.ldap_config entry when no mount
survives - but the task rendered the file unconditionally, so the ldap-off
variant wrote a host file nothing references. Both sides now share the
condition.
The mechanism was stranded too. render_replicated_templates exists to copy a
rendered file to the swarm peers, and those branches hang off
`not (IS_STACK_HOST | bool)`. Since 1da3c2eda this task file is included only
`when: IS_STACK_HOST | bool`, and include_tasks is dynamic, so for every other
host the file never exists - the peer branches became unreachable. Nothing was
lost by that: type: config renders as configs.<name>.file in both modes, and in
swarm `docker stack deploy` reads it on STACK_HOST and distributes it over raft.
php.ini one task above is the identical case and already used plain
stack_host_template, so this makes the file internally consistent and drops an
include_tasks, a slurp and a loop over a one-element list.
The notify stays: role_templates_expanded defaulted it to compose-up, which is
now spelled out.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A handler name was doing two jobs at once: the line an operator reads in the
play output, and the identifier a `notify:` binds to. The two pull in opposite
directions -- the log wants "restart the SSH service, with diagnostics", the
binding wants a token that survives quoting, YAML flow style and a shell
round-trip -- and a name that changes for readability silently changes which
handler runs.
Every handler that a notify reached by name now carries a `listen:` with that
exact wording, so the binding is explicit before anything is renamed. The
topics themselves become identifiers: `compose up` reads `compose-up`,
`Restart NFS ganesha` reads `restart-nfs-ganesha`, 22 of them across both
sides of every notify. Where the topic is assembled at runtime, both branches
moved together: the conditional in sys-service carries two literals, and the
LDAP renderer interpolates `import-{{ folder }}-ldif-files`, which is what
makes those handlers reachable at all.
Freed from the binding, the names take the emoji prefix every other task line
already has.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Add tests/lint/repository/test_task_file_numbering.py: numbered task
files use exactly two leading digits, a numbered *_core.yml must be
00_core.yml, per directory the numbers are unique/consecutive and start
at 01 (00 with a core file), numbered includes appear in ascending
order, and shared helpers live in tasks/utils/ instead of the numbered
chain.
Apply it across every role: rename NN_core.yml to 00_core.yml with a
consecutive down-shift of the siblings, move letter-suffix children into
a subdirectory named after their parent task, relocate helpers included
from more than one place into tasks/utils/, and update every reference
(include_tasks, path_absolute, .ansible-lint-ignore, requirement docs).
Legacy narration comments in the touched files are deleted; the upstream
xwiki.properties reference block opts out via nocheck. The unused
MAILU_IP6_PUBLIC var (only ever named in now-removed comments) is dropped.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>