GuestOps/docs/account-security.md
2026-09-30 10:24:27 +01:00

6.5 KiB

Account security and self-service

Milestone 19 is implemented on the milestone/19-account-security development branch for the planned 0.3.0 release. It must not be merged into main or enabled in production until the 0.2.0 Gate B candidate is approved and tagged.

Roles and authorization

GuestOps uses fixed server-enforced roles. Hiding a control in the browser is not an authorization boundary.

Role Access
Owner All hotel, integration, provider approval, automation, privacy, team, and security operations.
Manager Inbox and reviewed Gmail sending; knowledge and FAQ test-mode management; hotel display settings; activity and health; management of Agent and Auditor accounts.
Agent Inbox, drafts, status changes, reviewed Gmail sending, PMS/payment lookups, and preparation of proposals.
Auditor Read-only activity and aggregate workspace health. No inbox bodies, guest workflow, team, or provider controls.

Existing Staff records are normalized to Agent in sessions and permission checks. New invitations write Agent, Manager, or Auditor. Managers can manage only Agent and Auditor accounts. Only Owners can manage Managers, reset another user's MFA, enable integrations or writes, approve PMS/payment actions, enable live FAQ sending, or change security controls.

Role, password, disable/restore, and MFA changes rotate the security stamp and invalidate affected sessions. Tenant scope is always derived from the authenticated session; client-provided hotel identifiers are not authorization inputs.

TOTP MFA

MFA uses six-digit, 30-second TOTP with SHA-1 compatibility, a one-step clock window, and atomic last-step replay prevention. Enrollment creates ten single-use recovery codes. The data-protection key ring protects TOTP secrets; only SHA-256 recovery-code hashes are stored. Enrollment and regeneration return recovery codes once.

When Identity__RequireMfa=false, current password login remains available. When it is true, successful password verification creates only a five-minute, HttpOnly, SameSite=Strict protected challenge cookie. A normal eight-hour session is created only after authenticator or recovery-code verification. Existing sessions without mfa=true are rejected on their next request. Unenrolled users are routed through enrollment after password verification.

Do not enable enforcement until every user has enrolled, recovery-code storage has been verified, and an independent security review has passed. Password recovery preserves MFA.

An Owner may reset MFA for a Manager, Agent, or Auditor after identity verification. Owner MFA reset is deliberately excluded from web controls. A server administrator performs the audited recovery:

read -r -p 'Verified owner email: ' RECOVERY_EMAIL
export RECOVERY_EMAIL
docker compose run --rm --no-deps -e RECOVERY_EMAIL api --reset-owner-mfa
unset RECOVERY_EMAIL

This clears the Owner's enrollment, rotates the security stamp, ends existing sessions, and writes an audit event. It never prints a secret or recovery code. The administrator must verify identity using the approved procedure before running it.

Transactional email and self-service recovery

POST /api/auth/recovery always returns the same accepted response. Known and unknown addresses receive the same response. Requests are limited independently by client IP and a SHA-256 partition of the normalized address.

Production invitation and reset APIs return only userId, deliveryState, and expiresAt. Development preview may return a direct link for interface testing. Issuing another link invalidates the previous account token.

The API encrypts recipient, subject, body, and token link into an AccountMail outbox record. The worker atomically claims each record with a lease and sends it with a stable Message-ID. Clearly pre-submission failures receive at most five bounded retries. Any exception after SMTP submission begins is treated as ambiguous and moved to NeedsReview; it is never automatically resent. Expired records are not submitted. Workspace health exposes aggregate counts only.

SMTP configuration is private environment state:

SMTP_ENABLED=false
SMTP_HOST=smtp.example.invalid
SMTP_PORT=587
SMTP_USERNAME=...
SMTP_PASSWORD=...
SMTP_FROM_ADDRESS=guestops@example.invalid
SMTP_FROM_NAME=GuestOps

The transport requires authenticated STARTTLS and normal platform certificate validation. There is no insecure-certificate option. Do not put credentials in JSON acceptance records, API responses, screenshots, logs, or source control.

Mandatory security notices are queued for password, MFA, role, disable/restore, and recovery events. They have no preference switch.

User preferences

GET/PUT /api/me/preferences stores displayTimeZone and defaultInboxFilter with optimistic concurrency. Timezones are validated by server timezone data. Supported filters are All, NeedsAttention, DraftReady, and Completed. An empty display timezone uses the hotel's timezone. The session response includes effective permissions, MFA state, stored preferences, and the effective timezone.

Safe rollout

  1. Deploy with SMTP_ENABLED=false and IDENTITY_REQUIRE_MFA=false.
  2. Configure SMTP through private environment values and send only to synthetic accounts.
  3. Verify success, expiration, retry, restart, duplicate-claim, revocation, and ambiguous-outcome evidence; then enable SMTP.
  4. Enroll every user and verify their recovery-code storage procedure.
  5. Complete independent security review and record its evidence.
  6. Set IDENTITY_REQUIRE_MFA=true. Confirm password-only sessions are rejected and unenrolled users enter enrollment.
  7. Run the full role matrix, tenant-isolation, preferences, monitoring, audit, backup/restore, and restart checks.

SMTP or MFA failure must never weaken authorization or create a password-only bypass. A lost Owner authenticator uses only the server command above.

Acceptance

Copy deploy/account-security-acceptance.example.json to the restricted evidence store, replace all placeholders, and keep guest data, addresses, tokens, secrets, raw mail, and screenshots outside Git. Validate the completed record with:

python3 deploy/account_security_acceptance.py /restricted/path/account-security-acceptance.json

Retain the record, validator output, checksum, independent security approval, and evidence references against the same full release commit and release-record SHA-256. The validator checks structure and approval separation; it does not inspect or prove the underlying evidence.