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:
- Rate definitions — high commercial sensitivity; leakage to a competitor or another tenant directly damages revenue.
- Quotes — short-lived but legally binding while live; tampering is fraud-relevant.
- 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
| Caller | Identity | Token type |
|---|---|---|
| Booking BFF (guest flow) | guest session or anonymous booking principal | OAuth2 access token issued by iam-service |
| Backoffice web | logged-in staff user | OAuth2 access token, role claims included |
| Electron desktop | property service identity | long-lived refresh token bound to property + device id |
reservation-service (S2S) | service identity svc/reservation-service | mTLS + short-lived workload JWT |
desktop-sync-service (S2S) | service identity svc/desktop-sync-service | mTLS + short-lived workload JWT |
ai-orchestrator-service (S2S) | service identity svc/ai-orchestrator-service | mTLS + short-lived workload JWT |
All tokens are validated by the standard AuthGuard interceptor:
- Signature verified against
iam-service's JWKS (cached 10 min, refreshed onkidmiss). audMUST equalpricing-service.expenforced strictly; clock skew tolerated up to 30 s.tenant_idclaim MUST equal theX-Tenant-Idheader.- 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:lockand/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
| Material | Algorithm | Key store | Rotation |
|---|---|---|---|
| TLS public endpoints | TLS 1.3, ECDHE+AES-256-GCM | Google-managed certs | quarterly |
| mTLS service mesh | TLS 1.3, SPIFFE x509-SVID | Workload Identity Federation | 24 h |
| JWT signing (verified) | EdDSA / RS256 | iam-service JWKS | 90 d |
| At-rest DB | AES-256-GCM via CMEK | Cloud KMS, ring melmastoon-prod-pricing | 365 d auto-rotate |
| Pub/Sub messages | AES-256-GCM via CMEK | Cloud KMS | 365 d |
| GCS event mirror | AES-256-GCM via CMEK | Cloud KMS | 365 d |
| Idempotency / inbox tables | column-level: none (no PII) | — | — |
| Desktop SQLite mirror | AES-256 via SQLCipher | OS 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
| Class | Examples | Controls |
|---|---|---|
| Confidential — Tenant | Rate plans, rules, promo codes, AI suggestions | RLS + CMEK + ABAC |
| Confidential — Customer | Promo redemptions linked to reservations | RLS + reservation-service shares only reservationId (no PII) |
| Restricted — System | Outbox/inbox, idempotency keys | Service-only, no public exposure |
| Public — Aggregated | Anonymised quote analytics in BigQuery | Aggregation 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.
Moneymicro-unit fields must be non-negative integers ≤9_223_372_036_854_775_807(sanity bound).multiplierfields rejected if< 0or> 10(anomalous; prevents pricing detonation by bug).discountPctrejected if< 0or> 100.promoCodeconstrained to^[A-Z0-9_-]{3,32}$(case-insensitive on lookup, stored upper).- A per-tenant negative-total guard trips
MELMASTOON.PRICING.NEGATIVE_TOTALif any derivation produces agrandTotalMicro <= 0; the quote is refused. - Rate-limits per API_CONTRACTS §6 prevent enumeration of promo codes.
8. Secrets management
| Secret | Storage | Rotation |
|---|---|---|
| Postgres password | Secret Manager → Workload Identity injection | 30 d |
| Redis AUTH | Secret Manager | 90 d |
| Pub/Sub auth (gated by IAM) | Workload Identity (no secret) | n/a |
| FX provider API key | Secret Manager, per provider | 90 d |
| AI orchestrator session secret | Secret Manager | 30 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-alertsSNS 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_tagconstraint onfee_rulesensure 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_rulestable 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 ineurope-west4(Eemshaven) for DR. All snapshots remain in the EMEA jurisdiction set; no transfer to US regions.
11. Threat model summary (top 5)
| Threat | Mitigation |
|---|---|
| Cross-tenant rate leakage via SQL injection | Parameterised queries everywhere + RLS + CMEK key namespace + integration test that asserts cross-tenant SELECT returns zero rows |
| Promo code brute-force enumeration | Rate-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 signals | Signal payload is a fixed-shape struct, not free text; no booker-supplied strings reach the model |
| Rate-rule tampering by ex-employee with stale token | Short-lived JWT + token revocation list + step-up on archive/accept + full audit trail |