generated from jric11/baseProject
153 lines
9.1 KiB
Markdown
153 lines
9.1 KiB
Markdown
# Administrator security and shared capacity — October 20, 2026
|
||
|
||
Implements the first four remaining multi-tenant review recommendations. All
|
||
database verification uses disposable local databases. This work does not migrate
|
||
the development database or enroll any real account's authenticator.
|
||
|
||
## 1. Administrator MFA and recent authentication
|
||
|
||
- Added maintained `spomky-labs/otphp` 11.5.0 and its locked dependencies.
|
||
- Every authenticated request passes an MFA gate before application dispatch.
|
||
Super System Administrators and active club administrators must enroll and
|
||
complete MFA; an ordinary global User role does not bypass a club administrator
|
||
membership. Enrolled ordinary members must also complete their chosen MFA.
|
||
- Existing password login establishes the first factor only. Direct URLs and
|
||
AJAX requests cannot use administrator functionality before the gate completes.
|
||
- Setup requires the current password and a working authenticator code. The
|
||
setup page offers a locally generated QR code and a manual key for authenticator
|
||
apps using SHA-1, six digits and 30-second periods. The QR uses the same pending
|
||
secret, the application issuer and account email. A white quiet zone preserves
|
||
scan contrast on dark pages. Pending setup expires after ten minutes. No
|
||
third-party QR service receives the secret, and QR images are not saved to disk.
|
||
- Secrets use AES-256-GCM with a random nonce and account ID as authenticated
|
||
data. `HCR_MFA_KEY` is a separate, required 32-byte encryption key, encoded as
|
||
base64. Database records do not contain plaintext secrets.
|
||
- Verifies the current or adjacent time step, rejecting counters already used.
|
||
Verification locks the factor row. Ten random 128-bit recovery codes are
|
||
displayed once; only SHA-256 hashes are stored. Consumption and mandatory audit
|
||
commit together. Failed audit writes roll back consumption.
|
||
- Limits authentication attempts per account and IP. Successful MFA rotates the
|
||
session ID and CSRF token. Password login clears earlier MFA verification;
|
||
password resets preserve the factor and its replay counter.
|
||
- Account security is linked from the home page. Lost authenticator replacement
|
||
requires the password and an unused recovery/current authenticator code. It
|
||
retains the existing factor while the authorized session configures a new one.
|
||
Confirmation atomically replaces the factor, refreshes recovery codes and
|
||
revokes other sessions. See [MFA repairs](MFA-REPAIRS-2026-10-20.md).
|
||
- Global account/security changes and membership role/status changes require
|
||
password plus MFA confirmation within five minutes. The original write is
|
||
never replayed automatically after authentication; the user submits it again.
|
||
- MFA enrollment/replacement invalidates other account sessions. Session versions
|
||
are captured under lock, avoiding adoption of a concurrent password-reset
|
||
version. Recovery does not use email-only factor removal.
|
||
|
||
## 2. Absolute session lifetime
|
||
|
||
- `AuthService` records an authentication start timestamp at password login and
|
||
enforces an eight-hour maximum lifetime independently of the existing
|
||
30-minute idle timeout.
|
||
- Activity, MFA and recent-authentication confirmation do not extend that start
|
||
time. Legacy sessions without a start timestamp must sign in again.
|
||
|
||
## 3. Bounded collections and exports
|
||
|
||
- Added shared count/paging support using the same authorized SQL for both.
|
||
- Club navigation, roster members, seasons, dues, payment histories, corrections,
|
||
administrator invitation lists, pending invitations and ticket messages now
|
||
have independent 25-record pages with first/previous/next/last controls.
|
||
- Page numbers are validated and clamped. Links preserve the selected club and
|
||
other page/filter context. Personal member visibility applies to counts as
|
||
well as rows. Older records remain accessible through the pager.
|
||
- Dues-entry member/season selectors use their corresponding displayed pages;
|
||
navigate those pages to choose older seasons or additional members.
|
||
- Added a read-only, authorized report-catalog route for page navigation. Its
|
||
club list counts only clubs the administrator can manage.
|
||
- Roster CSV/Excel/PDF and financial CSV exports reject oversized collections
|
||
before loading them, with a default 1,000-row limit. Queries retain a second
|
||
hard bound so concurrent growth cannot create unbounded allocations.
|
||
`HCR_EXPORT_MAX_ROWS` accepts 1–5,000. Oversized exports return a clear error,
|
||
never a silently truncated file; a large-export workflow is not added here.
|
||
|
||
## 4. Shared club resource budgets
|
||
|
||
Each club has independent hourly counters for:
|
||
|
||
| Resource | Default per hour | Configuration |
|
||
|---|---:|---|
|
||
| Invitation email attempts | 100 | `HCR_CLUB_EMAIL_HOURLY` |
|
||
| New tickets | 100 | `HCR_CLUB_TICKET_HOURLY` |
|
||
| Ticket replies | 1,000 | `HCR_CLUB_REPLY_HOURLY` |
|
||
| Exports, across all formats | 20 | `HCR_CLUB_EXPORT_HOURLY` |
|
||
|
||
- Configured budgets accept 1–100,000; invalid configuration fails closed.
|
||
- Existing individual rate limits remain. Changing users or formats does not
|
||
reset the club counter. Another club has a separate budget.
|
||
- Atomic database increments return each connection's own assigned slot, so
|
||
simultaneous requests cannot exceed the configured budget.
|
||
- Permission checks precede consumption. Budgets count authorized attempts,
|
||
including failed delivery or later validation; they are deliberately not
|
||
refunded by transaction retries or failed writes.
|
||
- Counters occupy one row per club/resource and reset on the next attempt after
|
||
an hour, so request history does not grow without a bound.
|
||
|
||
## Deployment steps
|
||
|
||
1. Preserve a backup, then run `bin/migrate-security-capacity.php` with migration
|
||
credentials after the existing migrations. It adds `user_mfa`, `mfa_recovery`
|
||
and `club_resource_usage`; rerunning it preserves existing data.
|
||
2. Inject a unique random `HCR_MFA_KEY` through the application's secret store.
|
||
Generate 32 cryptographically random bytes and base64-encode them. Never use
|
||
the disposable test key. Keep a protected recovery copy separate from SQL
|
||
backups; changing or losing the key prevents decrypting existing factors.
|
||
Development can instead set `MfaKeyFile` in its private local settings to a
|
||
protected file outside the application directory. An explicit environment key
|
||
takes precedence. Production does not allow this development fallback.
|
||
3. Apply the new table grants in `deploy/runtime-grants.sql.example` to the
|
||
restricted runtime account. The health check now verifies these tables and,
|
||
in Production, the MFA key format.
|
||
4. Deploy the complete locked dependencies and updated source together. The
|
||
release builder includes the migration and new classes. Application version
|
||
is now 1.04.
|
||
5. Administrators sign in and enroll their own authenticator, saving their
|
||
recovery codes offline. Keep server clocks synchronized. If both the device
|
||
and all recovery codes are lost, recovery requires a trusted operator to
|
||
verify identity and perform an audited database recovery; there is no public
|
||
email-only bypass.
|
||
|
||
Production host acceptance, sender-domain/inbox checks, off-host recovery and
|
||
realistic sustained load testing remain separate deployment work. These changes
|
||
do not establish production hosting readiness by themselves.
|
||
|
||
## Verification
|
||
|
||
Regression verification covers **675 checks and ten concurrency scenarios**:
|
||
|
||
- Existing 584 authentication, maintenance, tenant isolation, financial,
|
||
browser, backup and deployment checks, plus nine existing concurrency races.
|
||
- 24 HTTP MFA/session checks, 41 service checks, 11 large-list/export HTTP checks
|
||
and 15 shared-budget HTTP checks.
|
||
- Twelve simultaneous requests from two administrators compete for five export
|
||
slots: exactly five succeed and seven receive the limit response.
|
||
- MFA checks include valid code verification, counter replay rejection,
|
||
single-use recovery, audit-failure rollback, account-bound encryption,
|
||
password-reset factor preservation, lost-device replacement, recent
|
||
authentication, promotion-triggered enrollment and both session timeouts.
|
||
- Source validation covers 60 PHP files, 57 application JavaScript files and
|
||
39 XSL stylesheets, plus bundled browser dependency integrity. Composer
|
||
installation reported no security advisories for the locked dependencies.
|
||
|
||
The existing regression suites and all new suites passed across the full run
|
||
and targeted follow-up runs. Test databases are disposable and no real email
|
||
is sent. The isolated local database server was stopped after verification.
|
||
|
||
The later [development migration and acceptance run](SECURITY-MIGRATION-2026-10-20.md)
|
||
verified the persistent key-file configuration with eight additional regression
|
||
checks and 68 acceptance checks against the backed-up development database.
|
||
|
||
The subsequent QR setup change passed ten browser checks using an independent
|
||
decoder, 24 existing MFA/security HTTP checks and 16 deployment checks. Browser
|
||
coverage includes scanning, enrollment using the scanned secret, phone-width
|
||
layout, stable pending setup details, replacement and absence of external QR
|
||
requests. QR rendering was also verified on ositsdevels without changing a real
|
||
account. No new application dependency or database migration was required.
|