Skip to main content

property-service — SECURITY_MODEL

Companion: DATA_MODEL · API_CONTRACTS · SYNC_CONTRACT · ../../docs/07-security-compliance-tenancy.md · ../../docs/standards/ERROR_CODES.md

This document is the binding security specification for property-service: identity, authentication, authorization (RBAC + ABAC), tenant isolation enforcement, encryption posture, secret handling, audit obligations, residency, and threat-model summary.


1. Identity & Authentication

CallerCredentialVerification
Tenant operator (web / desktop)OIDC ID token issued by iam-service; access token (JWT, RS256, 15 min TTL) carrying tenant_id, user_id, roles[], scope[]JWKS rotation every 24 h via iam-service's /.well-known/jwks.json; tokens with kid not in JWKS rejected
Service-to-serviceWorkload Identity (GCP) → short-lived signed JWT; aud=property-serviceAsymmetric verification against iam-service JWKS; iss allow-list
Public / consumer meta layerNone (anonymous) — only published, non-PII property fields exposed via bff-consumer-service cacheN/A (no direct calls to property-service)
AI orchestratorWorkload Identity bound to ai-orchestrator-service@melmastoon.iam.gserviceaccount.comSame as service-to-service
Sync (Electron desktop)Same operator JWT plus an X-Device-Id header verified against iam-service device registry; mismatch → MELMASTOON.IAM.DEVICE_NOT_REGISTEREDThe BFF performs device verification before invoking property-service

The service does not accept API keys, basic auth, or any long-lived shared secret in production. The development docker compose accepts a static dev JWT only when NODE_ENV=development.


2. Authorization

2.1 Roles (tenant-scoped)

Defined by iam-service; this table is the binding interpretation for the property domain.

RoleScope of property actions
tenant.ownerAll actions on all properties in tenant
tenant.adminAll actions; cannot transfer ownership
property.managerAll non-destructive actions on assigned properties (`property:read
front_deskproperty:read, property.room:status:write (only active <-> out_of_order) on assigned property
housekeeping_supervisorproperty:read, property.room:status:write (only via housekeeping-service event consumer; no direct API access)
marketingproperty:read, property.photo:write, property.translation:write, property.amenity:write
auditorproperty:read only; full read across the tenant

2.2 Permission catalog (this service)

property:read
property:write
property:publish
property:unpublish
property:archive
property.room:create
property.room:archive
property.room:status:write
property.room.notes:write
property.roomType:write
property.amenity:write
property.policy:write
property.photo:write
property.photo.order:write
property.translation:write
property.geo:write
property.ai:invoke

2.3 ABAC

Role grants are necessary but not sufficient. Every authorization check evaluates an attribute predicate:

authorize({
subject: { userId, roles, tenantId, propertyAssignments },
action: 'property.room:status:write',
resource: { tenantId: room.tenantId, propertyId: room.propertyId, roomId: room.id }
});

Attribute predicates encoded in the policies/property.rego (OPA bundle, served by iam-service):

  • subject.tenantId == resource.tenantId (always)
  • resource.propertyId IN subject.propertyAssignments OR subject.role IN ['tenant.owner','tenant.admin','auditor']
  • For property:publish: property must have ≥1 active room (DOMAIN invariant duplicated in policy for defense-in-depth)
  • For property.room:status:write: requested transition must be permitted by the role's transition matrix
  • For AI capabilities: tenant must have ai.enabled = true setting

Rejection produces MELMASTOON.GENERAL.AUTHORIZATION_DENIED (HTTP 403, Problem+JSON, no resource leakage in body).

2.4 Public read (consumer meta layer)

bff-consumer-service queries the projected document in search-aggregation-service, not property-service directly. Only fields explicitly tagged public:true in the projection contract are exposed; phone numbers, internal notes, and operator-only translations never leave the tenant boundary.


3. Tenant Isolation (defense in depth)

Four layers, all mandatory.

  1. Edge. API gateway strips and re-issues X-Tenant-Id from the verified JWT; request bodies must echo a matching value or be rejected with MELMASTOON.IAM.TENANT_MISMATCH.
  2. Application. RequestContextMiddleware binds tenantId into AsyncLocalStorage; every repository read/write asserts tenantId === ctx.tenantId and refuses cross-tenant references in aggregate constructors.
  3. Database. Every connection from the pool runs SET LOCAL app.tenant_id = '<uuid>'. RLS policies on every multi-tenant table use current_setting('app.tenant_id')::uuid. Policies are FORCEd so the service role cannot bypass.
  4. Outbox. The outbox writer asserts payload.tenantId == current_setting('app.tenant_id')::uuid inside the transaction. Mismatch → abort + alert.

A nightly tenant_isolation_audit job picks 200 random rows per multi-tenant table and verifies that querying them under a different tenant context returns zero rows. Any breach pages the security on-call.


4. Encryption

4.1 In transit

  • TLS 1.3 only between API gateway, BFFs, and property-service (mTLS via Istio service mesh).
  • TLS 1.3 to Cloud SQL via Cloud SQL Auth Proxy with IAM authentication; no password auth.
  • TLS 1.3 to Memorystore (Redis with AUTH + TLS).
  • TLS 1.3 to Pub/Sub.
  • Desktop ↔ BFF: HTTPS (TLS 1.3) with certificate pinning to the melmastoon.ghasi.io root.

4.2 At rest

  • Cloud SQL: CMEK (Cloud KMS key projects/melmastoon/.../keyRings/data/cryptoKeys/property-cmek); rotation every 90 days.
  • Cloud Storage (photos, owned by file-storage-service): CMEK with separate key.
  • Memorystore: encryption at rest enabled.
  • Desktop SQLite: SQLCipher with per-device 256-bit key in OS keychain.

4.3 Field-level

FieldTreatment
properties.contact.phone, contact.emailStored plaintext (operational necessity), masked in logs (***@…, +93***)
policies.resolved.deposit.cardLast4This service does not store any payment instrument. Deposit policy stores method + amount only
audit_events.diffIf a redacted field is present in a diff, the diff stores *** placeholder + a sealed reference to a KMS-wrapped blob (only retrievable via a privileged audit tool)

5. Secrets

  • All secrets resolved at startup from GCP Secret Manager via Workload Identity. Never read from env files in production.
  • Required secrets: Cloud SQL password (rotation handled by Cloud SQL IAM, the password is a backup), Memorystore AUTH string, Pub/Sub publisher key (Workload Identity-derived), JWT verification keys (fetched from JWKS, not Secret Manager).
  • Secret Manager IAM grants roles/secretmanager.secretAccessor only to the property-service runtime SA.
  • Secret rotation does not require a redeploy: the service refreshes Secret Manager-backed config every 5 min.

6. Audit

Every state-changing operation persists an audit_events row in the same transaction as the aggregate write. Schema in DATA_MODEL §4.11. Audit rows are append-only (RULE-enforced at DB level). Required fields:

  • actor_user_id, actor_kind, resource_type, resource_id, action
  • before_hash, after_hash (SHA-256 of canonicalized JSON of the aggregate snapshot)
  • diff (JSON Patch RFC 6902, with redactions noted)
  • request_id, trace_id

Audit is mirrored asynchronously to audit-service via melmastoon.audit.events.appended.v1. Loss of the mirror is non-blocking; the source of truth remains the local audit_events table.

Auditable actions (this service):

property.created | property.updated | property.published | property.unpublished | property.archived
property.translation.upserted | property.amenity.set | property.policy.override.set
property.photo.added | property.photo.removed | property.photo.reordered | property.photo.set_hero
property.room_type.created | property.room_type.updated | property.room_type.archived
property.room.created | property.room.updated | property.room.status.changed | property.room.archived
property.room_group.created | property.room_group.updated
property.ai.suggestion.staged | property.ai.suggestion.accepted | property.ai.suggestion.rejected

7. Compliance & Residency

  • Data classification. Properties, room metadata, amenities, photos: tenant_internal (not personal). Policies: tenant_internal. Audit + AI provenance: regulated.
  • Residency. Property writes pinned to me-central1 (Doha) for AF/IR/TJ tenants; europe-west4 for EU tenants; chosen by tenant-service at provisioning. property-service honors the tenant's dataResidency setting in its connection routing.
  • GDPR. Property records are not personal data, but uploaded photos may contain guests. The operator must tag containsGuestLikeness=true at upload; on a subject.erasure.requested.v1, the service archives such photos and emits melmastoon.property.photo.removed.v1 with reason gdpr_erasure.
  • Local laws. AF/TJ/IR-specific moderation and content rules are enforced by the AI moderation layer (orchestrator), not in this service.

8. Threat Model (STRIDE summary)

ThreatSurfaceMitigation
SpoofingAPI call from another tenantJWT verification + X-Tenant-Id echo check + RLS
TamperingModified pull/push payload from desktopTLS, HMAC of operation batch, server-side state machine validation
RepudiationOperator denies a publish actionAppend-only audit_events, audit-service mirror, signed event chain
Information disclosureCross-tenant property lookup via crafted propertyIdRLS (forced) + application-layer attribute check; nightly isolation audit
Denial of servicePhoto bulk upload, geo-search floodRate limits at gateway, per-route quotas, signed-URL upload offloads bytes to Cloud Storage
Elevation of privilegeOperator without property:publish calls publish endpointOPA policy check, role enumeration alarm on repeated 403s

9. Specific Sensitive Flows

9.1 Publish

  • Requires property:publish + ABAC predicate.
  • Domain pre-conditions: ≥1 active room, hero photo set, geo present, default-locale translation present.
  • Audit row records full snapshot.
  • Emits property.published.v1; downstream services see the event with actorUserId so they can attribute changes.

9.2 Photo upload

  • Operator calls POST /properties/:id/photos → service returns a one-shot signed URL from file-storage-service (validity 5 min, single-use, max 10 MB, allowed MIME image/jpeg|png|webp|heic).
  • Upload completes → file-storage-service emits melmastoon.media.asset.scanned.v1 with verdict clean|infected|suspicious.
  • property-service only marks the photo ready on clean; otherwise quarantined and operator is notified.

9.3 OOO toggle from front desk

  • Requires property.room:status:write. Idempotency key required for offline replay safety.
  • Writes audit + emits property.room.taken_out_of_order.v1.

10. Logging Hygiene

  • Tokens and signed URLs never appear in logs.
  • PII fields (contact.phone, contact.email) are masked by the LoggingInterceptor.
  • AI prompts are not logged (the orchestrator owns prompt logs in a separate, restricted bucket).
  • Every log line carries tenant_id, request_id, trace_id, actor_user_id (when known), route.

Cross-references: error code semantics in docs/standards/ERROR_CODES.md; platform-wide compliance in docs/07-security-compliance-tenancy.md; RLS templates in docs/06-data-models.md.