New lint test_no_raw_goto.py forbids raw `page.goto(...)` in role playwright specs + persona flows (the gotoOnion wrapper in personas/utils is exempt) so navigation retries transient Tor-transport errors over onion. Migrates every raw call across ~75 roles (343 sites) to `gotoOnion(page, ...)` and teaches gotoOnion to detect relative onion URLs (`isOnionCanonical`). gotoOnion is a behaviour-identical drop-in on clearnet (single attempt, same return). Verified: no-raw-goto lint green, eslint 0 errors, make test green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add the global TIMEOUT_FACTOR and a shared timeouts.js helper: resolveTimeout(base) scales by TIMEOUT_FACTOR and, over a .onion target, an extra multiplier, so Tor's per-request latency no longer trips hard-coded timeouts. Migrate every raw timeout in the specs (timeout:/timeout=/setTimeout/waitForTimeout) to resolveTimeout, stage the helper in all three staging paths, and add test_no_raw_timeouts to forbid raw numeric timeouts. Clearnet behaviour is unchanged (factor 1: resolveTimeout(N) === N).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
User identity SPOT
- meta/users.yml gains zammad-api-bot (Basic-auth REST API bot) and
zammad-wizard-bypass-admin (auto_wizard.json setup-bypass admin).
- vars/main.yml reads identity (login/email/firstname/lastname) via
lookup('users', …); password stays in credentials/ to guarantee the
shell-safe alphanumeric form needed for the Ansible→bash→docker-exec
→Ruby ENV chain.
- auto_wizard.json.j2, apply_api_bot.rb, tasks/01_core.yml: consume the
lookup-resolved identity.
Boot reliability
- templates/zz_telegram_constant_shim.rb.j2: pre-loaded via RUBYOPT;
pre-declares the Telegram::Bot namespace and intercepts Kernel#require
to refuse telegram/* loads, sidestepping the broken telegram-bot-ruby
2.4.0 gem that crashes Zammad 6.5.0 boot. Telegram channel is inert.
- env.j2 sets RUBYOPT=-r/opt/zammad/zz_telegram_constant_shim.rb.
- tasks/01_core.yml stages the shim; compose.yml.j2 bind-mounts it into
zammad-init/rails/websocket/scheduler/nginx.
- tasks/01_core.yml adds an explicit AutoWizard.setup post-init step
(upstream zammad-init does NOT auto-process auto_wizard.json), which
is what V2 (no-auth) and V3 (LDAP-only) need to skip the setup wizard.
LDAP source wire-up (TODO #1)
- files/scripts/apply_ldap_source.rb creates an LdapSource record
pointing at svc-db-openldap (inetOrgPerson + groupOfNames mapping).
- tasks/01_core.yml runs it when services.ldap.enabled is true.
OIDC group→role mapping (TODO #3)
- templates/zz_oidc_group_role_mapping.rb.j2 patches Zammad's
User.create_from_hash / update_from_hash to translate the RBAC
group-path claim (e.g. /roles/web-app-zammad/administrator) into
the matching Zammad role (Admin / Agent / Customer fallback).
- apply_oidc_settings.rb pins uid_field=preferred_username so OIDC
matches the human-readable Keycloak username, NOT the sub UUID.
- compose.yml.j2 bind-mounts the initializer when OIDC is enabled.
Test coverage
- new test-login-via-ldap.js (driven only in LDAP-only V3).
- test-alias-domain.js accepts 2xx-direct OR 301-to-canonical
(sys-svc-proxy renders aliases as 301 sources, not server-name
aliases — server.yml updated to canonical=helpdesk + alias=
zammad.helpdesk).
- test-mail-to-ticket.js: stage-level SMTP trace logging; reaches
Mailu via host.docker.internal:465.
- test-ticket-reply-as-agent.js: scoped contenteditable lookup to
.article-add to avoid hitting the ticket-title field.
- _shared.js: signInAsApiBot drives /api/v1/signin via the server-side
APIRequestContext (shared cookie jar with the browser), unblocking
the SPA-OIDC interlock.
SQL outsourcing
- templates/sql/grant_createdb.sql.j2: the CREATEDB grant now lives in
a Jinja-templated .sql file; tasks/01_core.yml consumes it via
lookup('template', …). Role name is rendered into the SQL by Jinja
(NOT via database_query named_args), because psql parameter binding
quotes string values as literals (`'zammad'`) which is invalid SQL
for ALTER ROLE.
Misc
- meta/server.yml: canonical=helpdesk.<domain>, aliases=[zammad.helpdesk.<domain>]
to match the 022 requirement (Decision #1).
- TODO.md drained — all seven items either landed or are explicitly
documented as out-of-scope / brittle.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Rename credential basic_auth_password → api_bot_password and var
ZAMMAD_ADMINISTRATOR_PASSWORD → ZAMMAD_API_BOT_PASSWORD to reflect
that the value is the role-internal bot's local password, not the
administrator's (reused for the wizard-bypass admin only because both
need the same shell-safe alphanumeric form).
- Introduce ORGANIZATION constant (defaults to SOFTWARE_NAME) and migrate
organization-string consumers (mattermost team, shopware shop title,
dashboard login/logout descriptions, simpleicons "provided by",
user-administrator description/lastname).
- Add HOST_LL_CC_TAG (BCP-47 form, e.g. en-gb) and use in auto_wizard.
- Add NETWORK_TRUSTED_PROXY_CIDRS SPOT and consume from suitecrm,
shopware and zammad reverse-proxy configs.
- Extract ZAMMAD_WEB_PROTOCOL via lookup('tls', …, 'protocols.web')
instead of inline 'https' if … else 'http'.
- Use database_query for CREATEDB grant so it works on shared+non-shared
databases; back the SQL with files/sql/grant_createdb.sql SPOT.
- Read administrator firstname/lastname from users lookup in
auto_wizard.json.j2 instead of hardcoding.
- Drop redundant elasticsearch: block from web-app-zammad services.yml
(no consumer reads it).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds 7 new Playwright scenarios covering helpdesk-core workflows and
infrastructure regressions:
- test-csp-headers — CSP response + meta-tag parity
- test-alias-domain — server-name alias serves 2xx, not 30x
- test-rest-api-ticket-create — POST /api/v1/tickets via Basic auth
- test-ticket-create-as-customer — biber SPA → fill form → submit
- test-ticket-reply-as-agent — API-seeded ticket; admin posts a reply
- test-websocket-realtime — open ticket; API appends article;
expect realtime push into the open SPA
- test-mail-to-ticket — raw-socket SMTP to Mailu helpdesk
mailbox; poll Zammad until ticket
appears
Auto-configures Zammad's Email channel (inbound IMAP + outbound SMTP)
against Mailu via a new post-bootstrap rails-runner step. The Ruby for
both bootstrap steps lives in standalone files and is piped through
`container exec -i ... bundle exec rails r -`; the previous wrapper
shell scripts (`apply_*.sh`) are removed in favour of inline `shell:`
calls in tasks/01_core.yml. Env vars are forwarded into the container
via `container exec -e VAR ...` so the Ruby uses `ENV.fetch` and stays
value-free / portable.
vars/main.yml grows three constants:
- ZAMMAD_OMNIAUTH_INIT_CONTAINER_PATH — bind-mount target inside the
container (was hard-coded four times in compose.yml.j2)
- ZAMMAD_HELPDESK_USER / _EMAIL / _PASS — derived from the `helpdesk`
mail-bot user provisioned via meta/users.yml
- ZAMMAD_MAIL / ZAMMAD_EMAIL_ENABLED — derived from the central email
lookup + the services.email gate
playwright.env.j2 grows MAIL_SMTP_HOST/PORT/USER/PASS + HELPDESK_EMAIL
for the SMTP injection in the mail-to-ticket scenario, sorted ASC per
the env-key lint.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>