Skip to main content

maintenance-service · SECURITY_MODEL

Aligned with docs/07-security-compliance-tenancy.md. Defence in depth: identity at the edge, RBAC+ABAC at the API, RLS at the DB, encryption-at-rest with KMS CMEK, encryption-in-transit everywhere, audit-grade event trail.

1. Identity & authentication

CallerMechanismNotes
BFF (bff-backoffice-service)Service JWT signed by iam-service, OIDC-style claimsValidated at Kong + re-validated in NestJS guard
Other services (server-to-server)Workload Identity (GCP) → short-lived OIDC tokens → mTLSNo long-lived service accounts
Pub/Sub pushOIDC token issued by Pub/Sub, audience = our service URLVerified via google-auth-library
Cron (Cloud Scheduler)OIDC token, audience = our service URLSame path as Pub/Sub
Desktop syncUser JWT + device JWT (device-bound)sync-service issues ephemeral upstream token to us
GitHub Actions (CI deploy)Workload Identity Federation, no static keys

JWT claims required for user calls:

{
"sub": "usr_01HXY...",
"tenant": "tnt_01HXP...",
"props": ["prop_01HXP...", "prop_01HXQ..."],
"roles": ["staff_supervisor","gm"],
"device": "dev_01HXY...",
"iat": 1745330400,
"exp": 1745334000
}

2. Authorization (RBAC + ABAC)

Roles enforced at the controller layer via NestJS guard @RequireRoles([...]). Property-level ABAC checks props array vs. the request's resolved propertyId.

ResourceActionRequired role(s)ABAC
WorkOrdercreatestaff (any)propertyId ∈ jwt.props
WorkOrderassignstaff_supervisor, gm, ownerproperty scope
WorkOrderstart, block, resume, resolveassignee themselves OR staff_supervisor+property scope
WorkOrderverifygm, ownerproperty scope
WorkOrdercancelstaff_supervisor+property scope
WorkOrderescalatestaff_supervisor+ or systemproperty scope
WorkOrderrecord vendor invoicestaff_supervisor+, accountingproperty scope
PreventiveSchedulecreate/update/delete/triggergm, ownerproperty scope
Assetcreate/update/deletestaff_supervisor+property scope
Vendorcreate/update/deletegm, owner, accountingtenant scope
Partcreate/updatestaff_supervisor+property scope

Authorization failures emit MELMASTOON.IAM.AUTHZ_DENIED (HTTP 403) and log actor.userId, route, subjectId, requiredRoles, actorRoles.

3. Multi-tenant isolation

Three layers:

  1. Edge: Kong validates JWT and asserts X-Tenant-Id matches the JWT's tenant claim. Any mismatch → 403 before reaching the service.
  2. Application: NestJS request-scoped middleware sets request.tenantId. All repositories take tenantId as a required parameter; passing a different one than the request's is a programmer error caught in code review and a runtime assertion.
  3. Database: Postgres RLS on every tenant-scoped table. Connection wrapper executes SET LOCAL app.tenant_id = $1 at the start of every transaction. RLS policy: tenant_id = current_setting('app.tenant_id').

A failure of any layer must not leak data — tested by tenant-isolation.spec.ts (mandatory in CI).

4. Data classification

FieldClassStorageNotes
WorkOrder.title, descriptionInternalPostgres plaintextIndexed for full-text search; no PII expected, but the AI redactor scrubs guest names from outbound LLM calls
WorkOrder.vendorAcknowledgement.noteInternalPostgres plaintext
Vendor.phoneE164, whatsappE164PII (medium)Postgres plaintext (operational need)Field-level audit on read by non-staff_supervisor
Vendor.email, addressFreeTextPII (medium)Postgres plaintextSame
vendor_invoice_file_refConfidentialGCS bucket with KMS CMEKSigned URL with 5 min TTL when served
cost_*_amount_micro, vendor_invoice_*Financial / RegulatedPostgres plaintext + retained 7 yrsRLS still enforced; analytics-only consumers receive aggregates
aiProvenanceAuditPostgres jsonbIncludes model id, score; no raw inputs
roomId, propertyId, assetIdIdentifiers (non-PII)plaintext

5. Encryption

  • In transit: TLS 1.3 mandatory; mTLS for service-to-service via Workload Identity.
  • At rest: Cloud SQL CMEK with KMS key projects/<melmastoon>/locations/europe-west1/keyRings/data/cryptoKeys/maintenance-db. Annual rotation. GCS bucket for vendor invoices: same KMS keyring.
  • Field-level: none required in v1 (no PII stored beyond contact details that must remain queryable). If we add guest contact in v2, we'll use the platform field-encryption library with HMAC-SHA256 deterministic encryption for searchable fields.

6. Secrets

SecretSourceUsage
Cloud SQL passwordSecret Manager secrets/maintenance-db-passwordMounted as env var via Cloud Run secret-volume
Pub/Sub publisher SAWorkload IdentityNo static credential file
KMS key accessIAM binding on the runtime SA
AI orchestrator client tokenIssued per-request via Workload Identity
Notification client tokenIssued per-request via Workload Identity

No secret is checked into git. CI uses Workload Identity Federation.

7. Audit trail

Every state-changing call produces:

  1. A domain event (carried via outbox to Pub/Sub).
  2. An entry in audit-service topic melmastoon.audit.maintenance.v1 with actor.userId, actor.deviceId, route, subjectId, before/after (cardinality-limited diff), correlationId, ipAddress, userAgent.

Auditor queries are served from BigQuery (melmastoon_audit_v1.maintenance_*).

8. Vendor invoice files (PII-adjacent)

  • Uploaded directly to GCS by the BFF using a signed PUT URL we generate (5 min TTL, exact content-type & size constraints).
  • We never receive the file body in our service; we only persist the GCS URI.
  • Bucket policy: object lifecycle = 7 years; uniform bucket-level access; CMEK; access logging on.

9. Privacy / regulatory

  • GDPR (EU tenants): vendor PII is processed under "legitimate interest". Erasure requests trigger Vendor.active=false+ field redaction; FK-linked WOs keep an opaque tombstone.
  • Local laws (Afghanistan/Pakistan operating regions): we keep all data in europe-west1 by default; tenant-pinned residency is supported via tenant settings.
  • Children data: maintenance has no child PII surface.
  • Data residency: tenant.dataResidency = 'eu-west' | 'asia-south'. Cross-region replication respects this.

10. AI safety surfaces

  • All inputs to the AI orchestrator are PII-redacted by default.
  • HITL on any capability that drives a state mutation (severity, vendor outbound).
  • Per-tenant budget controls (cost cap), with fail-soft on exhaustion.
  • Provenance persisted; replay supported.
  • Outputs are validated against constrained schemas (e.g., WorkOrderSeverity is an enum; bad values discarded silently).

11. Threat model summary

ThreatMitigation
Cross-tenant data leak via app bugRLS at DB layer (defence in depth); tenant-isolation.spec.ts in CI
Cross-tenant data leak via cache key collisionCache keys include tenantId; lint rule no-unscoped-cache-key
Forged Pub/Sub pushOIDC token verification with audience match; private VPC ingress
Replay of state transitions (e.g., resolve twice)OCC version + idempotency keys + inbox dedupe
Vendor-impersonation outbound notificationVendor record is server-side; outbound channel/identifier comes from DB, not request body
Long-lived service account key abuseWorkload Identity, no static keys
Unauthorised file access (vendor invoice)Signed URL with 5 min TTL, auth-checked at issue, KMS CMEK
AI prompt injection via guest complaint textPII redaction + constrained-output models + HITL on consequential capabilities
OCC bypass via direct DB writeOnly the service has write access; CI lints against ad-hoc DB writes; DB role lacks BYPASSRLS
Audit gap on bulk operationsAll bulk writes go through use cases; no direct repository access from controllers

12. Incident readiness

  • Runbooks linked from docs/standards/ERROR_CODES.md per code.
  • Tenant-scoped data export (for regulator subpoena) is supported via MIGRATION_PLAN.md §off-boarding flow.
  • Tenant-scoped erasure: Vendor redacted, Part deactivated, WOs anonymised on assignee_user_id and vendor_ack_recorded_by after 7-year regulated retention expires.