Skip to main content

SECURITY_MODEL — staff-service

Sibling: APPLICATION_LOGIC · DATA_MODEL · API_CONTRACTS · OBSERVABILITY

Strategic anchors: 07 Security & Tenancy · 02 §13 Security Posture · standards/ERROR_CODES

staff-service enforces three layers of security: tenant isolation (RLS-backed), identity & RBAC (delegated to iam-service), and a PIN clock-in surface that is uniquely physical/operational. PIN handling is the single most-reviewed attack surface; everything else inherits the platform's hardened auth posture.


1. Identity & Tenant Scope

  • All API requests carry a Bearer <jwt> issued by iam-service. The JWT is verified against the published JWKS (cached 5 min, regional CDN-backed). Verification fails → 401 MELMASTOON.IAM.JWT_INVALID.
  • The token's tnt claim must equal the request's X-Tenant-Id header. Mismatch → 403 MELMASTOON.TENANT.MISMATCH.
  • A TenantScopeMiddleware issues SET LOCAL app.tenant_id = '<ten_…>' and SET LOCAL app.user_id = '<usr_…>' and SET LOCAL app.role = '<role>' on every DB connection acquisition (PgBouncer transaction-pooling-aware via SET LOCAL).
  • Every table in the staff schema has Row-Level Security enabled with policies that filter by app.tenant_id (DATA_MODEL §4). RLS is the last line of defense; a use case forgetting to scope by tenant cannot leak across tenants.

2. RBAC Matrix

Capabilities are namespaced staff.<domain>.<verb>. They are issued by iam-service per role (tenant.admin, property.gm, front_desk.manager, front_desk, housekeeping.manager, housekeeping, maintenance.manager, maintenance, etc.).

CapabilityRoles (default)What it allows
staff.staff.readall staff at the propertyList + read non-PII fields
staff.staff.read_piiproperty.gm, front_desk.manager, tenant.adminEmail, phone, emergency contact
staff.staff.writeproperty.gm, tenant.adminCreate / update / change-position
staff.staff.terminateproperty.gm, tenant.adminTerminate or reactivate
staff.staff.set_pinproperty.gm, front_desk.manager, self (staff.self.set_pin)Rotate PIN
staff.position.writetenant.adminMutate position catalog
staff.department.writeproperty.gm, tenant.adminMutate department catalog
staff.shift.readall staff at the propertyRead schedule grid
staff.shift.writeproperty.gm, front_desk.manager, housekeeping.manager, maintenance.managerCreate / generate / cancel shifts
staff.assignment.writesame as shift.writeAssign / unassign / swap / promote
staff.clock.write_selfall staff (implicit on punch self)Self-punch via JWT or PIN
staff.clock.write_otherproperty.gm, front_desk.manager, housekeeping.manager, maintenance.managerManager-override punch
staff.clock.readall staff (own only); managers (department); GM (property)Read entries
staff.leave.submitall staff (self only)Submit own leave request
staff.leave.decideproperty.gm, dept managers (own dept)Approve / reject
staff.handoff.writeall staff (own position)Add handoff note
staff.handoff.readall staff at the propertyRead handoff notes
staff.report.readproperty.gm, tenant.adminAttendance / staffing-gap reports
staff.suggestion.readproperty.gm, dept managersRead AI shift suggestions

The RbacPolicy decorator on every controller maps the endpoint to one or more capabilities. ABAC overlays:

  • Property-access overlay. A capability + property pair must exist in the actor's iam.user_property_access. A housekeeping.manager at property A cannot decide leave for staff at property B.
  • Department-access overlay for staff.leave.decide and dept-manager scoped reads.
  • Self overlay for staff.clock.write_self, staff.leave.submit, staff.staff.set_pin.

Denial → 403 MELMASTOON.COMMON.RBAC_DENIED with violations[] listing missing capabilities.


3. JWT & Session Lifecycle

  • Tokens issued by iam-service per iam-service SECURITY_MODEL.
  • Refresh tokens are device-bound (X-Device-Id must match the token's dev claim).
  • staff-service does not store sessions; it relies on JWT verification + JWKS rotation. On iam.user.session_revoked.v1 events (consumer pattern), we invalidate any cached PIN-attempt counters for the affected user (defensive cleanup).
  • On staff.terminated.v1 we explicitly call iamClient.revokeAllSessionsForUser(...); failure is retried with exponential backoff up to 12 attempts then surfaced as a staff.audit_events row with action='iam.session.revoke.failed' and an alert (OBSERVABILITY §6).

4. PIN Clock-In

PIN clock-in is the only auth surface unique to staff-service. It is scoped to a single physical kiosk (X-Device-Id) at a single property.

4.1 PIN Format

  • Exactly 6 digits (/^\d{6}$/).
  • Rejected: all-same (111111), strictly ascending (123456), strictly descending (654321).
  • The platform considers PINs low-entropy; brute-force resistance comes from rate limiting + lockout, not entropy.

4.2 Storage

PIN_HMAC = HMAC-SHA256(pepper_KMS_v3, staff_id_bytes || tenant_id_bytes || pin_bytes)
  • pepper_KMS_v3 is wrapped in Cloud KMS, region-pinned per data-residency (me-central1 for default tenants).
  • Stored in staff.staff.clock_in_pin_hmac (32 bytes).
  • clock_in_pin_set_at records the rotation moment.
  • The pepper version (v3) is included in the HMAC envelope so we can rotate without a forced PIN reset (re-derivation on first verify after rotation).

4.3 Verification Flow (server-side)

  1. Look up candidate Staff rows by (tenant_id, property_id) joined to staff_property_access.
  2. For each candidate (constant-time iteration), compute HMAC and compare with clock_in_pin_hmac using crypto.timingSafeEqual.
  3. On match: reset clock_in_pin_failed, return staff.
  4. On no match: increment a per-property attempt counter (Redis token bucket) — not per-staff, since we don't know which staff was attempted.
  5. On per-staff lockout breach (5 failures within 15 min): set clock_in_pin_locked_until = now + 15 min, emit MELMASTOON.STAFF.PIN_LOCKED audit, push notification to property GM.

4.4 Verification Flow (Electron offline)

Per SYNC_CONTRACT §6:

  • Device caches a per-staff pinHmacChallenge = HMAC(deviceKey, staff_id || pin_hmac_short_hash) at last sync.
  • Offline punch verifies the entered PIN against the local challenge.
  • The original pin_hmac and pepper are never shipped to the device.
  • On push, the server re-verifies; offline failures are logged but the punch is not retried.

4.5 Rate Limiting

ScopeLimit
Per (staff_id)5 failures → 15 min lockout
Per (property_id)30 PIN attempts / minute (Redis bucket)
Per (device_id)60 PIN attempts / minute
Per (actor) for management overrides60 / minute

Limits exceeded → MELMASTOON.COMMON.RATE_LIMITED (429) with Retry-After.

4.6 PIN Rotation

  • Self-rotate at POST /staff/{id}/pin (any staff for self, with current PIN required).
  • Manager-rotate without current PIN (property.gm, front_desk.manager) for cases like a staff member forgot the PIN. Manager rotation requires reason and writes an audit row.
  • Pepper rotation: KMS pepper is rotated annually. New PIN sets use the new version; old hashes are re-verified with the version embedded; on successful verify, the row is lazy re-hashed to the new version.

5. Manager-Override Punches

The POST /clock/punch:manager-override endpoint requires:

  • Capability staff.clock.write_other.
  • Non-empty reason.
  • occurredAtUtc within the last 7 days (older requires platform-admin).
  • An audit row with action='clock.override', before=null, after={ clockEntryId, kind, reason }.

Override punches emit the same staff.clock.{in,out,...}.v1 events with source='manager_override' and managerOverrideBy/managerOverrideReason set.


6. PII Handling

FieldClassificationStorageOutput
email, manager_emailPII (low)citext, indexed lowerReturned to staff.read_pii only
phone_e164PII (low)textReturned to staff.read_pii only
emergency_contactPII (high)bytea envelope-encrypted with KMS key per regionReturned to staff.read_pii only; decrypted at read
clock_in_pin_hmacsecret materialbyteaNever returned (only pinSet: boolean)
spoken_languages, skill_ids, certification_idsnon-PIIjsonb / text[]Returned in any read
staff_code, namesnon-PII (operational)textReturned in any read

DSAR (data subject access request) flow: a staff.dsar_request row is opened by iam-service upon receiving a verified user request; staff-service exports the staff record (decrypted) plus all clock entries, leave requests, certifications, and audit trail in JSON. Erasure: emergency_contact_enc is set to NULL, email/phone_e164 are nulled, staff_code is preserved (legitimate-interest retention for attendance audit). Termination is recorded if not already.


7. Encryption

  • In transit. TLS 1.3 only. mTLS between staff-service and iam-service / property-service over the VPC (Anthos Service Mesh in M2).
  • At rest. Cloud SQL CMEK (KMS HSM keyring). Memorystore AUTH + in-transit TLS. Cloud Storage CMEK. Pub/Sub messages encrypted with CMEK envelope.
  • Field-level. emergency_contact_enc envelope-encrypted with staff-service-pii-v1 KMS key. Decryption requires the actor to hold staff.read_pii.

8. Audit Logging

Every write use case emits a row to staff.audit_events (DATA_MODEL §4.13) within the same Tx as the state change. The audit writer is a domain port; the infra implementation is a Postgres trigger backed by the use case's annotation.

Audit-required actions (non-exhaustive):

  • staff.create, staff.update, staff.terminate, staff.reactivate, staff.set_pin, staff.position_change
  • shift.create, shift.cancel, shift.assign, shift.unassign, shift.swap, shift.promote_standby
  • clock.punch_self, clock.override, clock.system_auto
  • leave.submit, leave.approve, leave.reject, leave.cancel
  • cert.add, cert.expire_emit
  • ai.* (per AI_INTEGRATION §7)
  • iam.session.revoke.failed

Audit rows are immutable (append-only trigger; DATA_MODEL §5). Hot retention 90 d in Postgres; cold export to BigQuery (7 y).


9. Threat Model (selected)

ThreatMitigation
Cross-tenant data leak via missed scopeRLS on every table (DATA_MODEL §4); CI scan for any query lacking app.tenant_id setter
PIN brute-force at front deskPer-staff lockout + per-property/device rate limit (§4.5); KMS pepper
Stolen device with cached PIN challengesPer-device DEK in OS keystore; remote iam revoke triggers full local wipe at next pull
Manager-override abuseRequired reason + audit log + GM-only capability + alert on > 5 overrides / day / actor
PIN guessable (123456)Format validation rejects sequences and all-same; PIN rotation policy
Replay of offline punch with wrong time(staff_id, occurred_at_utc, kind) unique index; ±5 min skew tolerance; flagged late-replay
Cross-property staff confusion (same PIN reused)PIN HMAC is keyed with staff_id; identical PIN at different staff yields different HMAC
Schedule grid scrape via APIRate limit on GET /shifts; capacity GET protected by staff.shift.read
AI suggestion poisoningSuggestions are advisory + audited; no auto-apply; fairness report (AI_INTEGRATION §8)
Outbox / inbox dual-write inconsistencyTx-bound outbox; idempotent inbox; CI test in mandatory outbox.spec.ts and inbox.spec.ts
Termination cascade raceTerminateStaff is the only writer of terminated; cascade in same Tx; IAM revoke retried

10. Compliance Posture

  • GDPR-aligned PII handling per 07 §6 GDPR.
  • Local labour-law minimum retention: configurable per tenant (default 7 y for staff records, 7 y for clock entries).
  • Audit logs retained 7 y (BigQuery cold).
  • All keys CMEK; tenant data-residency honored at KMS region pinning.

11. Secret Material

SecretStorageRotationOwner
PIN HMAC pepperCloud KMSAnnual (lazy re-hash)Platform Security
Field-level encryption key (emergency_contact)Cloud KMS90 d (envelope re-wrap)Platform Security
DB credentialsSecret Manager30 dPlatform Ops
Pub/Sub service-account keySecret Manager (or workload identity)n/a (workload identity preferred)Platform Ops
AI orchestrator service-to-service mTLSSecret Manager (cert)90 dPlatform Security