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 byiam-service. The JWT is verified against the published JWKS (cached 5 min, regional CDN-backed). Verification fails → 401MELMASTOON.IAM.JWT_INVALID. - The token's
tntclaim must equal the request'sX-Tenant-Idheader. Mismatch → 403MELMASTOON.TENANT.MISMATCH. - A
TenantScopeMiddlewareissuesSET LOCAL app.tenant_id = '<ten_…>'andSET LOCAL app.user_id = '<usr_…>'andSET LOCAL app.role = '<role>'on every DB connection acquisition (PgBouncer transaction-pooling-aware viaSET LOCAL). - Every table in the
staffschema has Row-Level Security enabled with policies that filter byapp.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.).
| Capability | Roles (default) | What it allows |
|---|---|---|
staff.staff.read | all staff at the property | List + read non-PII fields |
staff.staff.read_pii | property.gm, front_desk.manager, tenant.admin | Email, phone, emergency contact |
staff.staff.write | property.gm, tenant.admin | Create / update / change-position |
staff.staff.terminate | property.gm, tenant.admin | Terminate or reactivate |
staff.staff.set_pin | property.gm, front_desk.manager, self (staff.self.set_pin) | Rotate PIN |
staff.position.write | tenant.admin | Mutate position catalog |
staff.department.write | property.gm, tenant.admin | Mutate department catalog |
staff.shift.read | all staff at the property | Read schedule grid |
staff.shift.write | property.gm, front_desk.manager, housekeeping.manager, maintenance.manager | Create / generate / cancel shifts |
staff.assignment.write | same as shift.write | Assign / unassign / swap / promote |
staff.clock.write_self | all staff (implicit on punch self) | Self-punch via JWT or PIN |
staff.clock.write_other | property.gm, front_desk.manager, housekeeping.manager, maintenance.manager | Manager-override punch |
staff.clock.read | all staff (own only); managers (department); GM (property) | Read entries |
staff.leave.submit | all staff (self only) | Submit own leave request |
staff.leave.decide | property.gm, dept managers (own dept) | Approve / reject |
staff.handoff.write | all staff (own position) | Add handoff note |
staff.handoff.read | all staff at the property | Read handoff notes |
staff.report.read | property.gm, tenant.admin | Attendance / staffing-gap reports |
staff.suggestion.read | property.gm, dept managers | Read 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. Ahousekeeping.managerat property A cannot decide leave for staff at property B. - Department-access overlay for
staff.leave.decideand 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-serviceper iam-service SECURITY_MODEL. - Refresh tokens are device-bound (
X-Device-Idmust match the token'sdevclaim). staff-servicedoes not store sessions; it relies on JWT verification + JWKS rotation. Oniam.user.session_revoked.v1events (consumer pattern), we invalidate any cached PIN-attempt counters for the affected user (defensive cleanup).- On
staff.terminated.v1we explicitly calliamClient.revokeAllSessionsForUser(...); failure is retried with exponential backoff up to 12 attempts then surfaced as astaff.audit_eventsrow withaction='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_v3is wrapped in Cloud KMS, region-pinned per data-residency (me-central1for default tenants).- Stored in
staff.staff.clock_in_pin_hmac(32 bytes). clock_in_pin_set_atrecords 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)
- Look up candidate
Staffrows by(tenant_id, property_id)joined tostaff_property_access. - For each candidate (constant-time iteration), compute
HMACand compare withclock_in_pin_hmacusingcrypto.timingSafeEqual. - On match: reset
clock_in_pin_failed, return staff. - On no match: increment a per-property attempt counter (Redis token bucket) — not per-staff, since we don't know which staff was attempted.
- On per-staff lockout breach (5 failures within 15 min): set
clock_in_pin_locked_until = now + 15 min, emitMELMASTOON.STAFF.PIN_LOCKEDaudit, 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_hmacand 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
| Scope | Limit |
|---|---|
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 overrides | 60 / 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 requiresreasonand 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. occurredAtUtcwithin 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
| Field | Classification | Storage | Output |
|---|---|---|---|
email, manager_email | PII (low) | citext, indexed lower | Returned to staff.read_pii only |
phone_e164 | PII (low) | text | Returned to staff.read_pii only |
emergency_contact | PII (high) | bytea envelope-encrypted with KMS key per region | Returned to staff.read_pii only; decrypted at read |
clock_in_pin_hmac | secret material | bytea | Never returned (only pinSet: boolean) |
spoken_languages, skill_ids, certification_ids | non-PII | jsonb / text[] | Returned in any read |
staff_code, names | non-PII (operational) | text | Returned 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-serviceandiam-service/property-serviceover 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_encenvelope-encrypted withstaff-service-pii-v1KMS key. Decryption requires the actor to holdstaff.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_changeshift.create,shift.cancel,shift.assign,shift.unassign,shift.swap,shift.promote_standbyclock.punch_self,clock.override,clock.system_autoleave.submit,leave.approve,leave.reject,leave.cancelcert.add,cert.expire_emitai.*(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)
| Threat | Mitigation |
|---|---|
| Cross-tenant data leak via missed scope | RLS on every table (DATA_MODEL §4); CI scan for any query lacking app.tenant_id setter |
| PIN brute-force at front desk | Per-staff lockout + per-property/device rate limit (§4.5); KMS pepper |
| Stolen device with cached PIN challenges | Per-device DEK in OS keystore; remote iam revoke triggers full local wipe at next pull |
| Manager-override abuse | Required 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 API | Rate limit on GET /shifts; capacity GET protected by staff.shift.read |
| AI suggestion poisoning | Suggestions are advisory + audited; no auto-apply; fairness report (AI_INTEGRATION §8) |
| Outbox / inbox dual-write inconsistency | Tx-bound outbox; idempotent inbox; CI test in mandatory outbox.spec.ts and inbox.spec.ts |
| Termination cascade race | TerminateStaff 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
| Secret | Storage | Rotation | Owner |
|---|---|---|---|
| PIN HMAC pepper | Cloud KMS | Annual (lazy re-hash) | Platform Security |
Field-level encryption key (emergency_contact) | Cloud KMS | 90 d (envelope re-wrap) | Platform Security |
| DB credentials | Secret Manager | 30 d | Platform Ops |
| Pub/Sub service-account key | Secret Manager (or workload identity) | n/a (workload identity preferred) | Platform Ops |
| AI orchestrator service-to-service mTLS | Secret Manager (cert) | 90 d | Platform Security |