Skip to main content

SECURITY_MODEL — reservation-service

Sibling: APPLICATION_LOGIC · DATA_MODEL · API_CONTRACTS · OBSERVABILITY

Strategic anchors: 07 Security, Compliance & Tenancy · ADR-0002 Multi-Tenancy · 09 Lock & Key · 10 Payments

reservation-service holds guest PII, partial document references, channel attribution, and is the saga orchestrator for booking. It is in scope for tenant isolation, GDPR/regional privacy regulation, audit-traceability, and AML-style anomaly review. It is out of PCI scope (no card data) and out of lock-vendor secret scope (no vendor SDKs imported).


1. Identity & authentication

CallerAuth mechanism
bff-tenant-booking-service (guest funnel)Service-to-service OIDC token from iam-service with aud=reservation-service and tenant_id claim; or anonymous-booking-session JWT for unauthenticated guests (max scope: reservation:create:own)
bff-backoffice-service (front-desk)Staff JWT with tenant_id, property_id array, roles[], device_id (Electron device-bound token); short-lived (15 min) refreshable
Pub/Sub push handlers (/internal/events/*)GCP service-account JWT with aud=reservation-service; signature verified by Cloud Run platform; principal must be pubsub-pusher@<project>.iam.gserviceaccount.com
Cloud Scheduler (/internal/jobs/expire-holds)Service-account JWT; principal must be scheduler@<project>.iam.gserviceaccount.com
Internal health (`/internal/healthready`)

X-Tenant-Id is cross-checked against the JWT's tenant_id claim on every request; mismatch returns 403 MELMASTOON.TENANT.MISMATCH. Anonymous booking sessions carry tenant_id inside the session token signed by iam-service; the bff cannot fabricate it.


2. Authorization (RBAC + ABAC)

The full matrix is reproduced in APPLICATION_LOGIC §6. Highlights:

ActionRoles allowedAttribute checks
Quote / Holdguest, anonymous_booking, staff_front_desk, gm, ownertenantId match; staff also need propertyId access
Confirm (event-driven)system (Pub/Sub principal)event signed by payment-gateway-service IAM principal
Cancel own reservationguestreservation.primaryGuest.guestId == jwt.guest_id
Cancel any reservationstaff_front_desk, gm, ownerpropertyId access
Modify (any sub-type)staff_front_desk, gm, ownerpropertyId access; manual_state_override requires gm or owner
Check-in / Check-out / Walk-instaff_front_desk, gm, ownerpropertyId access; device must be bound_for_offline for offline use
policyOverride on cancelgm, owner onlywritten justification mandatory; logs to audit-service
Merge / Splitgm, ownersame property; gm cannot merge across properties
Read by guest (/by-guest/:guestId)staff_front_desk, gm, owner, finance; guest (self)tenantId match

Authorization decisions are evaluated in the application layer guard (AuthorizationDecider) before the use case runs; denials produce an audit-service event.


3. Tenant isolation (defense in depth)

Four independent layers, every one capable of stopping a cross-tenant breach:

  1. JWT/X-Tenant-Id consistency — controller guard rejects mismatch (403).
  2. Postgres RLS — every table sets app.tenant_id via SET LOCAL; policies named <table>_tenant_isolation (DATA_MODEL §3) bind every row.
  3. Domain-layer TenantId invariant — every aggregate constructor refuses missing or mismatched tenant context (Invariant I1 in DOMAIN_MODEL §4).
  4. Outbox tenant tagging — every outbox row carries tenant_id; the relay refuses to publish events whose envelope tenant does not match the row.

The mandatory test/integration/tenant-isolation.spec.ts test exercises all four layers including direct ID guesses across tenants, RLS bypass attempts, and event publishing under a swapped session.


4. Data classification

DataClassStorageAt-rest encryption
Guest email, phonePII (high)reservation.guests.email_enc, phone_e164_encAES-256-GCM via tenant-scoped DEK from KMS; HMAC-SHA256 hash for indexed lookup
Guest namesPII (medium)reservation.guests.full_name_*Cloud SQL CMEK; not field-level (transliteration, sort, search require plaintext)
Document last4 + issuerPII (medium)reservation.guests.document_*Cloud SQL CMEK
Reservation totals, payment methodconfidentialreservation.reservations.*Cloud SQL CMEK
payment_intent_ids, key_credential_idsreferences onlyreservation.reservations.*Cloud SQL CMEK; no vendor secrets
Modification auditconfidentialreservation.reservation_modificationsCloud SQL CMEK; immutable
Outbox envelopesconfidentialreservation.outboxCloud SQL CMEK; PII tokenized at sink

4.1 Field-level encryption keys

  • One DEK per tenant for guests.email_enc and phone_e164_enc.
  • DEKs are wrapped under a tenant-scoped CMK in Cloud KMS (projects/<p>/locations/<r>/keyRings/melmastoon-tenants/cryptoKeys/tnt_<id>).
  • Key rotation policy: yearly automatic; on rotation, new writes use the new DEK; reads transparently decrypt with prior DEK versions.
  • email_hash = HMAC-SHA256(tenantSalt, lower(trim(email))).
  • phone_e164_hash = HMAC-SHA256(tenantSalt, normalizeE164(phone)).
  • tenantSalt is per-tenant in Secret Manager and rotated every 24 months (with rolling re-hash background job).

4.3 The hold-expiry sweeper

The hold-expiry worker runs as a separate Cloud Run job under role reservation_holds_sweeper which has BYPASSRLS only for reservation.reservation_holds. Every other table (including reservations itself) is accessed via per-row SET app.tenant_id. The sweeper transitions only the local reservation_holds row and emits hold_expired.v1; the consequent state change to reservations happens via the inbox handler running under the tenant role.


5. Secret handling

  • No payment processor or lock vendor SDK is imported. Compliance is enforced by an ESLint dependency-graph rule in CI.
  • Secrets used: Postgres credentials (Cloud SQL Proxy via Workload Identity), Pub/Sub publisher credentials (Workload Identity), Cloud KMS access, Secret Manager access for tenantSalt. All bound via Workload Identity; no service-account JSON keys in any deploy artifact.

6. Audit trail

Every state-changing action emits to audit-service via the canonical event:

{
subjectKind: 'reservation',
subjectId: 'rsv_…',
tenantId: 'tnt_…',
action: 'reservation.confirmed' | 'reservation.cancelled' | 'reservation.modified.<sub>'
| 'reservation.checked_in' | 'reservation.checked_out' | 'reservation.no_show'
| 'reservation.policy_override' | 'reservation.merged' | 'reservation.split',
actor: ActorRef,
before?: object,
after?: object,
policyAppliedRef?: string,
reason?: string,
occurredAt: ISODate,
}

Audit events are part of the daily Merkle anchor in audit-service (07 §9). The manual_state_override modification type is always audit-flagged.


7. Privacy & regulatory

  • GDPR / equivalent: Guest.erased_at (DSR right-to-erasure); PII columns zeroed; reservations remain for revenue ledger; modifications kept for regulated retention. The erasure job emits melmastoon.guest.erased.v1 consumed by notification-service, analytics-service, search-aggregation-service to purge their projections.
  • Data residency: Iran-tenant data (tenant.region == 'gcp-asia-south1' or designated) is constrained to in-region Postgres replica and in-region Pub/Sub topics; cross-region replica is opt-in per tenant.
  • Children data: additional_guests.is_child_without_id == true flag triggers stricter export controls (no inclusion in analytics extracts).
  • No PCI: card data never reaches us; the paymentIntentIds we store are tokens.

8. AI safety surfaces

ai-orchestrator-service enforces:

  • DLP redaction of inputs leaving our service.
  • Per-tenant budgets (see AI_INTEGRATION §4).
  • HITL gating for auto_block anomaly verdicts and any AI verdict that would advance state irreversibly.

We never persist a free-form AI response without normalizing it through a typed schema; outputs failing schema validation are dropped and logged.


9. Threat model (summary)

ThreatMitigation
Cross-tenant data read via guessed rsv_ idRLS + JWT/tenant guard + branded id repository checks
Replay of payment-captured event from another tenantPub/Sub subscription is tenant-agnostic; envelope's tenantId cross-checked against the loaded reservation's tenant; mismatched messages dead-lettered
Operator account compromiseShort-lived JWTs + device binding + per-action audit + GM/owner approval for irreversible overrides
Mass scraping via /by-guest enumerationPer-token rate limits enforced by API gateway; query path requires guestId (not enumerable) or hashed contact
Forged state transitions from desktop offline queueServer-side state machine re-runs every command on push; OCC + RLS prevent unauthorized writes
Inflated/forged reservation total tamperingTotals are pricing-service-derived and re-validated server-side at confirm; modifications produce payment deltas with audit
Lock vendor compromiseWe hold no vendor secrets; lock-integration-service is a separate trust boundary

10. Cross-references