generated from jric11/baseProject
153 lines
8.4 KiB
Markdown
153 lines
8.4 KiB
Markdown
# Multi-tenant production review — October 20, 2026
|
||
|
||
Follow-up: the four confirmed defects below are addressed in
|
||
[tenant invitation and ticket repairs](TENANT-REPAIRS-2026-10-20.md). The findings
|
||
describe the reviewed version. Administrator MFA, session lifetime and capacity
|
||
recommendations are implemented in [the security/capacity follow-up](SECURITY-CAPACITY-2026-10-20.md).
|
||
Production acceptance recommendations remain applicable.
|
||
|
||
The development tenant migration and owner promotion are complete. The earlier
|
||
61 development acceptance checks and the broader regression results remain
|
||
useful evidence, but they did not cover the edge cases reproduced below.
|
||
This review changed no application code or development database records.
|
||
Reproductions used generated disposable databases on a separate loopback server.
|
||
|
||
## Confirmed defects, in recommended repair order
|
||
|
||
### 1. P1 — Member invitation restores inactive administrator privileges
|
||
|
||
Location: `classes/ClubInvitations.php:49`; related guard in
|
||
`classes/RosterService.php:62`.
|
||
|
||
The invitation acceptance upsert always activates the membership and preserves
|
||
an existing `administrator` role. This conflicts with the rule that only the
|
||
Super System Administrator can reactivate/change administrator memberships.
|
||
|
||
Reproduced:
|
||
|
||
1. The Super System Administrator deactivates an administrator membership while
|
||
retaining its historical administrator role. A second administrator remains.
|
||
2. That other club administrator sends a normal **member** invitation to the
|
||
inactive administrator's email. No administrator invitation or Super approval
|
||
is involved.
|
||
3. The recipient accepts. The resulting membership is `active/administrator`,
|
||
and `TenantAccess::canManage()` returns true.
|
||
|
||
This restores management rights within that club; the reproduction did not
|
||
grant platform-wide privileges or access to an unrelated club. It is still a
|
||
confirmed bypass of the chosen administrator-assignment policy.
|
||
|
||
Fix: make reactivation/role assignment honor the newly authorized invitation
|
||
role. An inactive historical administrator must not regain administrative rights
|
||
through a member invitation. Require fresh Super approval for an administrator
|
||
grant, and test inactive, demoted, existing active and shared-account cases.
|
||
|
||
### 2. P2 — Invitation acceptance and membership edits use opposite lock order
|
||
|
||
Locations: `classes/ClubInvitations.php:42`–`43` and
|
||
`classes/RosterService.php:58`–`60`.
|
||
|
||
Acceptance locks the club and then the user; membership editing locks the user
|
||
and then the club. Controlled concurrent execution of the actual service methods
|
||
reproduced a MariaDB deadlock (error **1213**): acceptance failed with a
|
||
`PDOException` while the membership update succeeded. The services roll back the
|
||
failed transaction, but do not retry it; the user receives an application error.
|
||
|
||
Fix: establish a consistent lock order across invitation, membership and account
|
||
flows. Add bounded retry handling for retryable transaction failures, with the
|
||
authorization checks rerun inside the retried transaction. Extend concurrency
|
||
coverage to acceptance versus editing/revocation, rather than only two accepts.
|
||
|
||
### 3. P2 — Ticket character limits conflict with the global byte limit
|
||
|
||
Locations: `classes/Base.php:250`, `classes/ClubTickets.php:4` and `:18`/`:27`.
|
||
|
||
Tickets advertise/validate an 8,000 Unicode-character message, but the dispatcher
|
||
rejects any individual value exceeding 8,192 bytes before ticket validation runs.
|
||
A valid 3,000-character message consisting of `界` occupies 9,000 UTF-8 bytes and
|
||
was rejected with **HTTP 400 / Invalid request** through the real HTTP handler.
|
||
|
||
Fix: align field-specific character and byte limits and the HTTP form limits.
|
||
Retain request-shape and maximum-size protections; report field-specific errors
|
||
instead of rejecting supported text before the field validator can inspect it.
|
||
Cover both creation and replies with multibyte boundary tests.
|
||
|
||
### 4. P2 — Tickets beyond the newest 100 cannot be found through the UI
|
||
|
||
Locations: `classes/ClubTickets.php:8` and `classes/Roster.php::tickets()`.
|
||
|
||
The service applies a hard `LIMIT 100`; the page offers no older-page navigation
|
||
or search. With 101 owned tickets, the list returned 100 and excluded the oldest.
|
||
The older ticket remains in storage and an authorized direct link still works,
|
||
but members/administrators cannot discover it through the ticket list.
|
||
|
||
Fix: add stable server-side paging/search, keeping the club/author access filter
|
||
in every page and count query. Include tests for the second page, private-ticket
|
||
visibility and cross-club paging attempts.
|
||
|
||
## Administrator security before public launch
|
||
|
||
Authentication remains password-only (`classes/AuthService.php:37`), and sensitive
|
||
global account/role changes do not require recent authentication. The session
|
||
validator has an idle timeout, but no independently enforced absolute lifetime.
|
||
The Super System Administrator can access all tenants, so compromise of that
|
||
account has platform-wide impact.
|
||
|
||
I recommend MFA for the Super System Administrator and club administrators,
|
||
recent authentication for privilege/email/security changes, a bounded absolute
|
||
session lifetime, and a tested recovery process. This is a security-hardening
|
||
recommendation, not a newly demonstrated login bypass. OWASP recommends MFA for
|
||
[administrative/high-privilege users](https://cheatsheetseries.owasp.org/cheatsheets/Multifactor_Authentication_Cheat_Sheet.html#quick-recommendations).
|
||
|
||
## Shared-service capacity and recovery
|
||
|
||
Roster members, dues, financial histories, invitation lists and several exports
|
||
still load whole collections with `fetchAll()`. Ticket messages are also unpaged.
|
||
Current invitation/ticket limits are chiefly per actor, not a total tenant budget.
|
||
No realistic large-club/load result was established by this review.
|
||
|
||
Before general onboarding, establish expected club sizes and concurrent use,
|
||
measure expensive pages/exports, add bounded paging/streaming where needed, and
|
||
set tenant-level resource limits so one busy club cannot consume shared capacity.
|
||
This follows the resource-isolation concerns in
|
||
[OWASP's multi-tenant guidance](https://cheatsheetseries.owasp.org/cheatsheets/Multi_Tenant_Security_Cheat_Sheet.html).
|
||
|
||
The full development backup was restored successfully during migration. A
|
||
production recovery plan should additionally explain how to recover one club's
|
||
records without rolling back unrelated clubs, and test that procedure.
|
||
|
||
## Production acceptance work still required
|
||
|
||
These are deployment checks, not assertions that external hosting configuration
|
||
is absent or that the approved development connection needs changing.
|
||
|
||
- Preserve a reviewed release containing the currently untracked tenant classes,
|
||
migration, tests and assets; run CI on that exact release.
|
||
- On the actual production host, verify HTTPS/proxy trust, secure session storage,
|
||
headers/cookies, secret injection and private-directory denial. The local PHP
|
||
router is not evidence that Apache/IIS/Nginx deployment rules are active.
|
||
- Run complete tenant/invitation/ticket/financial flows using the actual restricted
|
||
runtime database account, with separate migration and backup credentials and
|
||
verified TLS for a remote production database.
|
||
- Verify real invitation, signup-verification and reset inbox delivery, trusted
|
||
links, sender-domain authentication and delivery-failure monitoring. Recent
|
||
development acceptance deliberately sent no external messages.
|
||
- Install/verify backup, retention, cleanup and health schedules, off-host
|
||
protected recovery copies, alert routing and log rotation. Exercise failure
|
||
alerts for SMTP, mandatory audit writes, backup jobs and storage exhaustion.
|
||
- Explicitly assign the intended club administrators before onboarding members.
|
||
Development currently has one existing club and no real memberships; the
|
||
security tests correctly used owned temporary assignments.
|
||
|
||
## Review evidence and limits
|
||
|
||
- Confirmed the privilege restoration, opposite-lock-order deadlock, Unicode HTTP
|
||
rejection and 101-ticket list truncation in disposable local databases.
|
||
- Current `composer audit --locked` returned no advisories or abandoned packages.
|
||
This does not cover all vendored browser assets or establish that hosting is safe.
|
||
- Did not rerun the full regression suite, load-test production, send real email,
|
||
or modify development records. The new edge cases need permanent regression
|
||
coverage with their fixes.
|
||
- Removed the disposable databases and stopped their separate loopback server
|
||
after the review.
|