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
| Caller | Credential | Verification |
|---|---|---|
| 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-service | Workload Identity (GCP) → short-lived signed JWT; aud=property-service | Asymmetric verification against iam-service JWKS; iss allow-list |
| Public / consumer meta layer | None (anonymous) — only published, non-PII property fields exposed via bff-consumer-service cache | N/A (no direct calls to property-service) |
| AI orchestrator | Workload Identity bound to ai-orchestrator-service@melmastoon.iam.gserviceaccount.com | Same 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_REGISTERED | The 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.
| Role | Scope of property actions |
|---|---|
tenant.owner | All actions on all properties in tenant |
tenant.admin | All actions; cannot transfer ownership |
property.manager | All non-destructive actions on assigned properties (`property:read |
front_desk | property:read, property.room:status:write (only active <-> out_of_order) on assigned property |
housekeeping_supervisor | property:read, property.room:status:write (only via housekeeping-service event consumer; no direct API access) |
marketing | property:read, property.photo:write, property.translation:write, property.amenity:write |
auditor | property: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 = truesetting
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.
- Edge. API gateway strips and re-issues
X-Tenant-Idfrom the verified JWT; request bodies must echo a matching value or be rejected withMELMASTOON.IAM.TENANT_MISMATCH. - Application.
RequestContextMiddlewarebindstenantIdinto AsyncLocalStorage; every repository read/write assertstenantId === ctx.tenantIdand refuses cross-tenant references in aggregate constructors. - Database. Every connection from the pool runs
SET LOCAL app.tenant_id = '<uuid>'. RLS policies on every multi-tenant table usecurrent_setting('app.tenant_id')::uuid. Policies areFORCEd so the service role cannot bypass. - Outbox. The outbox writer asserts
payload.tenantId == current_setting('app.tenant_id')::uuidinside 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.ioroot.
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
| Field | Treatment |
|---|---|
properties.contact.phone, contact.email | Stored plaintext (operational necessity), masked in logs (***@…, +93***) |
policies.resolved.deposit.cardLast4 | This service does not store any payment instrument. Deposit policy stores method + amount only |
audit_events.diff | If 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.secretAccessoronly to theproperty-serviceruntime 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,actionbefore_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-west4for EU tenants; chosen bytenant-serviceat provisioning.property-servicehonors the tenant'sdataResidencysetting in its connection routing. - GDPR. Property records are not personal data, but uploaded photos may contain guests. The operator must tag
containsGuestLikeness=trueat upload; on asubject.erasure.requested.v1, the service archives such photos and emitsmelmastoon.property.photo.removed.v1with reasongdpr_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)
| Threat | Surface | Mitigation |
|---|---|---|
| Spoofing | API call from another tenant | JWT verification + X-Tenant-Id echo check + RLS |
| Tampering | Modified pull/push payload from desktop | TLS, HMAC of operation batch, server-side state machine validation |
| Repudiation | Operator denies a publish action | Append-only audit_events, audit-service mirror, signed event chain |
| Information disclosure | Cross-tenant property lookup via crafted propertyId | RLS (forced) + application-layer attribute check; nightly isolation audit |
| Denial of service | Photo bulk upload, geo-search flood | Rate limits at gateway, per-route quotas, signed-URL upload offloads bytes to Cloud Storage |
| Elevation of privilege | Operator without property:publish calls publish endpoint | OPA 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 withactorUserIdso they can attribute changes.
9.2 Photo upload
- Operator calls
POST /properties/:id/photos→ service returns a one-shot signed URL fromfile-storage-service(validity 5 min, single-use, max 10 MB, allowed MIMEimage/jpeg|png|webp|heic). - Upload completes →
file-storage-serviceemitsmelmastoon.media.asset.scanned.v1with verdictclean|infected|suspicious. property-serviceonly marks the photoreadyonclean; otherwisequarantinedand 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 theLoggingInterceptor. - 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.