Skip to main content

housekeeping-service — SECURITY_MODEL

Tenant isolation by Postgres RLS; AuthN by iam-service JWTs; AuthZ by role-based policy at the API edge plus invariant-based checks in the domain. Pub/Sub push subscriptions verified via OIDC. Aligned with docs/07-security-compliance-tenancy.md.

This document is the binding security spec for housekeeping-service. Any deviation requires an ADR.


1. Identity & authentication

CallerAuthNNotes
Electron desktop user (supervisor, housekeeper, property manager, owner)Bearer JWT signed by iam-serviceCarries tenant_id, staff_id, role[], properties[], iat, exp (15 min).
Tenant booking surface (mid-stay request)Bearer JWT (guest-scoped, downgraded)Limited to POST /tasks with kind=mid_stay_clean for the guest's own active reservation.
Pub/Sub pushOIDC token issued to service account housekeeping-events@<project>.iam.gserviceaccount.comVerified against Google JWKS; audience must match the push URL.
Internal cron callers (Cloud Run Jobs)Workload identity via service account housekeeping-cron@<project>.iam.gserviceaccount.comCalls /internal/cron/* over the internal load balancer only.

JWT validation at the gateway and re-validated in this service (defence-in-depth). Clock skew tolerance ±60 s. Replay protection via jti cache in Redis (5 min).

2. Tenant isolation

  • Every table has a tenant_id column. RLS policies (see DATA_MODEL.md) enforce USING (tenant_id = current_tenant_id()) and WITH CHECK (...) on every read and write.
  • The application sets app.tenant_id per connection at the start of every request via a Postgres SET LOCAL app.tenant_id = $1 inside the UnitOfWork.begin().
  • Cross-tenant leak is blocked at three layers: gateway X-Tenant-Id ↔ JWT match, application TenantContext propagation, and Postgres RLS. Removing any one still leaves the others; the tests/integration/tenant-isolation.spec.ts suite asserts each layer.

3. Authorization matrix (role × action)

Roles, in increasing power: housekeeper < housekeeping_supervisor < property_manager < owner < tenant_admin. Optional flags: inspector (additive). The booking surface uses a guest pseudo-role.

Endpoint / actionhousekeepersupervisorprop_mgrownertenant_adminguest
GET /boardown propertyyesyesyesyes
GET /tasks/:idown assigned + own propertyyesyesyesyes
POST /tasks (turnover, deep_clean, …)yesyesyesyes
POST /tasks (mid_stay_clean for own active reservation)yesyesyesyesyes (via BFF)
POST /tasks/:id/assignself only (claim flow)yesyesyesyes
`POST /tasks/:id/startpauseresume`own taskyesyesyes
POST /tasks/:id/completeown taskyesyesyesyes
POST /tasks/:id/failown taskyesyesyesyes
POST /tasks/:id/escalateown taskyesyesyesyes
POST /tasks/:id/maintenance-requiredown taskyesyesyesyes
PATCH /tasks/:id/priorityyesyesyesyes
POST /rooms/:id/status (manual flip)reason requiredyesyesyes
POST /rooms/:id/blockyesyesyesyes
POST /checklistsyesyesyes
POST /lost-and-foundyesyesyesyesyes
`POST /lost-and-found/:id/matchdispose`yesyesyesyes
`POST /linen/*/issuereturn`own taskyesyesyesyes
POST /tasks/:id/inspectionsinspector flag requiredinspector flag requiredyesyesyes
GET /stats/turnoveryesyesyesyes

Enforcement: @Roles() decorator at the controller (presentation), plus invariant checks in domain (e.g., aggregate refuses an assign to a staff member outside the tenant's staff set).

4. Object-level access

Beyond role checks:

  • A housekeeper can only act on tasks where assignee_staff_id = JWT.staff_id OR they hold a self-claim from the open queue.
  • A housekeeping_supervisor is scoped to properties[] from the JWT.
  • A property_manager and above are scoped to their assigned properties.
  • A tenant_admin can act on all properties of the tenant.

Object-level checks are run in the use case (not only the controller), so event-driven and sync-driven calls are equally enforced.

5. Data classification

ClassExamples in this serviceTreatment
PublicAggregate counts, room numbers (without status)Cacheable
InternalTasks, statuses, checklistsRLS, JWT-scoped
SensitiveLost-and-found descriptions, photosPhotos via signed URLs only; descriptions logged with redaction
PII (limited)Claimant name & phone for matched lost itemsLogged with redaction; access audited; retention per tenant policy
PCINoneNot handled here
PHINoneNot handled here

Photos are stored by media-service in GCS with object lifecycle (90 days warm, archive after) and are referenced here only by mediaId. Signed URLs minted on demand with 5-minute TTLs.

6. Encryption

  • At rest: Cloud SQL CMEK; backups CMEK; SQLite on Electron desktop encrypted via OS keychain (DPAPI on Windows, Keychain on macOS, libsecret on Linux).
  • In transit: TLS 1.3 everywhere (HTTPS, Cloud SQL, Pub/Sub). Internal-only endpoints additionally require mTLS via the internal load balancer.
  • Field-level: none required (no card data, no health data).

7. Secrets

All in Google Secret Manager, mounted as files at runtime:

  • DB connection (host, user, password)
  • JWT public key set (rotation: weekly)
  • Pub/Sub OIDC audience and SA email
  • Routing port API token (for orchestrator)

Service code uses @melmastoon/shared-secrets to load and watch for rotation. No secrets in env vars longer-term, no secrets in logs (enforced by redactor.middleware.ts).

8. Logging redaction

Allowlist-based: only the field set in log-redactor.allowlist.ts is permitted out. Lost-and-found description, claimant name/phone, and notes are masked unless the log level is DEBUG and LOG_REDACT=off is set in non-production environments.

9. Pub/Sub trust

  • Push subscriptions hit https://housekeeping.<env>.melmastoon.com/internal/events/<topic-short> over HTTPS.
  • We verify the OIDC token: signature against Google JWKS, aud == push URL, email == housekeeping-events@<project>.iam.gserviceaccount.com.
  • Replay protection via inbox (topic, message_id).
  • DLQ melmastoon.dlq.housekeeping; on-call rotates ownership monthly.

10. Audit & compliance

  • Every state-changing use case writes to audit_events (local) and emits the corresponding event (which audit-service archives off-platform).
  • Manual room-status overrides require reason and are tagged in audit with cause="manual_override".
  • Quarterly DPIA review owned by Operations + Compliance; documented in docs/07-security-compliance-tenancy.md.

11. Threat model (top 10, condensed)

#ThreatControl
T1Cross-tenant read/writeRLS + JWT match + tests
T2Spoofed Pub/Sub pushOIDC verification
T3Replay of mutating RESTIdempotency-Key, jti cache
T4Privilege escalation via direct DBDB user has no BYPASSRLS; admin actions audited
T5Lost-and-found PII leakAllowlist redaction; signed URLs for photos
T6Routing-suggestion misuse (auto-apply)HITL gate default supervisor_approval; per-suggestion audit
T7Sync conflict abuse (older write wins)Per-field policy + version check + audit on conflict
T8DoS via large checklists / linen opsPayload size caps; rate limits
T9Stale JWT after revokeShort TTL (15 min); revocation list checked at gateway
T10Local SQLite theft on desktopOS-keychain encryption; auto-lock on screen-lock