Skip to main content

SECURITY_MODEL — pricing-service

Sibling: API_CONTRACTS · DATA_MODEL · APPLICATION_LOGIC · SYNC_CONTRACT

Strategic anchors: 07 Security, Compliance, Tenancy · ADR-0002 Multi-tenancy model

The pricing-service guards three classes of asset:

  1. Rate definitions — high commercial sensitivity; leakage to a competitor or another tenant directly damages revenue.
  2. Quotes — short-lived but legally binding while live; tampering is fraud-relevant.
  3. AI dynamic-pricing artefacts — both commercially sensitive and compliance-relevant (Sharia, audit).

The platform-wide controls in 07 apply; this document specifies what's specific to pricing-service.


1. Identity and authentication

1.1 Token surfaces

CallerIdentityToken type
Booking BFF (guest flow)guest session or anonymous booking principalOAuth2 access token issued by iam-service
Backoffice weblogged-in staff userOAuth2 access token, role claims included
Electron desktopproperty service identitylong-lived refresh token bound to property + device id
reservation-service (S2S)service identity svc/reservation-servicemTLS + short-lived workload JWT
desktop-sync-service (S2S)service identity svc/desktop-sync-servicemTLS + short-lived workload JWT
ai-orchestrator-service (S2S)service identity svc/ai-orchestrator-servicemTLS + short-lived workload JWT

All tokens are validated by the standard AuthGuard interceptor:

  • Signature verified against iam-service's JWKS (cached 10 min, refreshed on kid miss).
  • aud MUST equal pricing-service.
  • exp enforced strictly; clock skew tolerated up to 30 s.
  • tenant_id claim MUST equal the X-Tenant-Id header.
  • For S2S, the workload SAN (SPIFFE id) is checked against an allow-list per endpoint.

1.2 Step-up authentication

Two operations require step-up (re-authentication or WebAuthn re-challenge within the last 5 minutes):

  • POST /v1/admin/pricing/dynamic-suggestions/{id}:accept — applies AI rate.
  • POST /v1/admin/pricing/rate-plans/{id}:archive — affects existing future bookings.

The step-up assertion is presented as X-Step-Up: <jwt> and validated by iam-service's introspection endpoint.


2. Authorization

Authorization is RBAC + ABAC. The full matrix lives in APPLICATION_LOGIC §6. Key principles:

  • Tenant isolation is enforced redundantly at four layers: API gateway header check, application-layer guard, RLS policy, and CMEK key namespace.
  • Property scoping is checked at the application layer (ActorRef.propertyAccess).
  • HITL roles (revenue_manager, gm, owner) are the only roles allowed to accept AI suggestions or archive rate plans with active future bookings.
  • The system role system (booking-saga) is the only non-human principal allowed to call /internal/v1/quotes:lock and /internal/v1/quotes:release.

Every authorization denial is published as melmastoon.audit.authorization_denied.v1 with actor, resource, action, decision_rule.


3. Tenant isolation guarantees

3.1 At the request boundary

[ Cloud Armor ] → [ API Gateway: extract tenant_id from JWT, set X-Tenant-Id ]
→ [ NestJS TenantGuard: assert header == claim ]
→ [ TenantContextInterceptor: ALS-bind tenant_id ]
→ [ Service code reads via cls.get('tenantId') ]

3.2 At the database

Every query is run as the pricing_app Postgres role with app.tenant_id set per session via SET LOCAL app.tenant_id = …. Forgetting the SET produces an empty-result regression test failure (a CI test plants seed rows for two tenants and asserts cross-tenant queries return zero rows). RLS policies are listed per table in DATA_MODEL.

3.3 At the cache

Memorystore keys are prefixed with pri:<tenant_id>:. The cache wrapper REFUSES to read a key whose prefix doesn't match the current tenant in ALS. Cache-key misuse is treated as a P0 bug.

3.4 At the events bus

Pub/Sub topic-level ACL allows pricing-service to publish all melmastoon.pricing.* topics. Each event envelope carries tenantId; subscribers use the platform-wide TenantConsumerGuard to assert the message tenant matches their own context before processing.

3.5 At the AI boundary

Outbound calls to ai-orchestrator-service strip tenantId from the prompt body but retain it in the orchestration session token. The model NEVER receives the tenant id; cross-tenant data leakage by prompt injection is therefore not exploitable.


4. Cryptography

MaterialAlgorithmKey storeRotation
TLS public endpointsTLS 1.3, ECDHE+AES-256-GCMGoogle-managed certsquarterly
mTLS service meshTLS 1.3, SPIFFE x509-SVIDWorkload Identity Federation24 h
JWT signing (verified)EdDSA / RS256iam-service JWKS90 d
At-rest DBAES-256-GCM via CMEKCloud KMS, ring melmastoon-prod-pricing365 d auto-rotate
Pub/Sub messagesAES-256-GCM via CMEKCloud KMS365 d
GCS event mirrorAES-256-GCM via CMEKCloud KMS365 d
Idempotency / inbox tablescolumn-level: none (no PII)
Desktop SQLite mirrorAES-256 via SQLCipherOS keychain (Windows DPAPI / macOS keychain)per-device, on enrolment

KMS access is gated by Cloud IAM bindings; only the pricing-service workload SA holds roles/cloudkms.cryptoKeyEncrypterDecrypter on the pricing keyring.


5. Data classification

ClassExamplesControls
Confidential — TenantRate plans, rules, promo codes, AI suggestionsRLS + CMEK + ABAC
Confidential — CustomerPromo redemptions linked to reservationsRLS + reservation-service shares only reservationId (no PII)
Restricted — SystemOutbox/inbox, idempotency keysService-only, no public exposure
Public — AggregatedAnonymised quote analytics in BigQueryAggregation thresholds (k-anonymity ≥ 25) before any export

Pricing-service deliberately stores no PII on the booker. Promo redemptions reference a reservationId only; resolving who that booker is requires authorised access to reservation-service. This narrows the blast radius of any leak.


6. Desktop security (Electron)

  • The desktop app is Electron (sandboxed renderer, contextIsolation enabled, nodeIntegration disabled).
  • Local pricing mirror lives in SQLCipher; the database key is wrapped with the OS keychain via keytar.
  • The renderer cannot directly read the SQLite file; all reads go through an IPC bridge that enforces property/tenant scoping.
  • A property device can be revoked centrally; the next sync attempt triggers a wipe of the local pricing schema (rate plans, rules, fees, taxes, fx, quotes, redemptions).

See ADR-0003 for the broader desktop security model. Tauri is not used.


7. Input validation and abuse controls

  • All inputs validated by Zod schemas before reaching the use-case layer.
  • Money micro-unit fields must be non-negative integers ≤ 9_223_372_036_854_775_807 (sanity bound).
  • multiplier fields rejected if < 0 or > 10 (anomalous; prevents pricing detonation by bug).
  • discountPct rejected if < 0 or > 100.
  • promoCode constrained to ^[A-Z0-9_-]{3,32}$ (case-insensitive on lookup, stored upper).
  • A per-tenant negative-total guard trips MELMASTOON.PRICING.NEGATIVE_TOTAL if any derivation produces a grandTotalMicro <= 0; the quote is refused.
  • Rate-limits per API_CONTRACTS §6 prevent enumeration of promo codes.

8. Secrets management

SecretStorageRotation
Postgres passwordSecret Manager → Workload Identity injection30 d
Redis AUTHSecret Manager90 d
Pub/Sub auth (gated by IAM)Workload Identity (no secret)n/a
FX provider API keySecret Manager, per provider90 d
AI orchestrator session secretSecret Manager30 d

No secrets in env vars on disk; all are mounted in-memory by the workload identity binding. The CI pipeline scans for hardcoded secrets and fails the build.


9. Audit logging

The following actions emit a synchronous audit event to audit-service:

  • Any CreateRatePlan, Publish, Archive.
  • Any rate rule change (create/update/retire).
  • Promotion create/activate/deactivate.
  • Tax/fee rule upsert.
  • Manual RefreshFxSnapshot.
  • AI suggestion accept/reject.
  • Authorization denial (3+ in 60 s from same actor → triggers a security-alerts SNS notification).

Audit fields: tenantId, actor, action, resourceType, resourceId, before, after, decisionRule, correlationId, ip, userAgent.


10. Compliance posture

  • PCI DSS — pricing-service does not handle card data; out of scope. (Card data flows through payment-gateway-service.)
  • GDPR / CCPA — no PII in pricing-service; data subject requests are routed to reservation-service/crm-service.
  • Sharia compliance — domain-level guards (see DOMAIN_MODEL §5) plus the sharia_tag constraint on fee_rules ensure no compounding deposits or interest-bearing fees on a Sharia-compliant plan; AI suggestions on Sharia plans are constrained to non-interest logic.
  • Local tax laws (AF/TJ/IR) — tax rules are governmental data; we maintain the tax_rules table per DATA_MODEL §7 with EXCLUDE constraints to enforce overlap-free validity windows.
  • Data residency — Cloud SQL primary in me-central2 (Doha), with cross-region read replica in europe-west4 (Eemshaven) for DR. All snapshots remain in the EMEA jurisdiction set; no transfer to US regions.

11. Threat model summary (top 5)

ThreatMitigation
Cross-tenant rate leakage via SQL injectionParameterised queries everywhere + RLS + CMEK key namespace + integration test that asserts cross-tenant SELECT returns zero rows
Promo code brute-force enumerationRate-limits + 24 h IP block on > 100 invalid codes/hour + audit alert
Race-condition over-redemption (cap bypass)Atomic SQL with INSERT … RETURNING + UPDATE … WHERE usage_count < usage_cap
AI prompt injection in suggestion signalsSignal payload is a fixed-shape struct, not free text; no booker-supplied strings reach the model
Rate-rule tampering by ex-employee with stale tokenShort-lived JWT + token revocation list + step-up on archive/accept + full audit trail