88 lines
6.5 KiB
Markdown
88 lines
6.5 KiB
Markdown
# 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:
|
|
|
|
```sh
|
|
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:
|
|
|
|
```text
|
|
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:
|
|
|
|
```sh
|
|
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.
|