# ZeroTier Device Membership ## Overview This document covers the ZeroTier device membership model — how devices are registered, approved, activated, and deactivated on ZeroTier networks. It explains the schema design, the distinction between `active` and `approved`, the user activation flow, and audit coverage. ## Schema Design The core model is `NetworkAccessRequest` (table: `network_access_requests`). It replaces the legacy two-table approach (`UserNetworkApproval` + `DeviceNetworkMembership`) with a single per-device, per-network row. ### `network_access_requests` table | Column | Type | Purpose | |---|---|---| | `id` | UUID | Primary key | | `organization_id` | FK -> organizations | Org scope | | `user_id` | FK -> users | Requesting user | | `device_id` | FK -> devices | Target device | | `portal_network_id` | FK -> portal_networks | Target network | | `granted_by_user_id` | FK -> users (nullable) | Approving manager | | `grant_type` | enum | `requested` or `assigned` | | `status` | enum | `pending`, `approved`, `rejected`, `revoked`, `suspended` | | `active` | boolean | Currently authorized on controller? | | `justification` | text | User's reason for requesting | | `join_seen` | boolean | Controller observed the device join | | `deleted_at` | timestamp (nullable) | Soft delete support | **Unique constraint:** `(user_id, device_id, portal_network_id, deleted_at)` — ensures exactly one active record per device per network. ### Supporting models | Model | Table | Purpose | |---|---|---| | `Device` | `devices` | A user-registered ZeroTier node (10-char node ID) | | `PortalNetwork` | `portal_networks` | A managed ZeroTier network (scoped to org) | | `ActivationSession` | `activation_sessions` | Temporary authorization window (TTL-based) | | `ZeroTierMembership` | `zerotier_memberships` | Cache of controller-side state | | `KillSwitchEvent` | `kill_switch_events` | Append-only audit of kill switch actions | ### Entity relationships ``` Device (1) ──> (N) NetworkAccessRequest (N) <── (1) PortalNetwork │ (1 or 0) │ ActivationSession │ (1 or 0) │ ZeroTierMembership ``` ## `active` vs `approved` — the key distinction These are orthogonal concepts: | Concept | Represents | Set by | Persists across sessions? | |---|---|---|---| | **`status = approved`** | Administrative permission to use the network | Manager approval | Yes — once approved, stays approved until revoked | | **`active = True`** | Device is currently authorized on the ZT controller | User activation | No — toggled on/off per session | A request can be in any of these states: | `status` | `active` | Meaning | |---|---|---| | `approved` | `false` | Manager said yes, but user hasn't activated yet | | `approved` | `true` | Manager said yes, device is actively connected | | `pending` | `false` | Awaiting manager decision | | `rejected` / `revoked` / `suspended` | `false` | Access denied or removed | The `active` flag is **not** a persistent grant — it's a run-time operational state tied to an `ActivationSession` with a finite TTL (default 8 hours). ## User activation flow — "turning on" ZeroTier ``` User API Service ZT Controller │ │ │ │ │ POST /orgs//approvals │ │ │ ├───────────────────────────>│ request_access() │ │ │ ├──> creates NetworkAccessRequest (status=PENDING) │ │ ├──> _ensure_zerotier_member() │ │ │ └──> provisions member (de-authorized) ────────────────>│ │ │ │ │ │ <── 201 { status: pending } │ │ │ │ │ │ │ [Admin approves] │ │ │ │ POST /orgs//approvals │ │ │ │ //approve │ │ │ ├───────────────────────────>│ approve_request() │ │ │ ├──> sets status=APPROVED │ │ │ <── 200 { status: approved } │ │ │ │ │ │ │ POST /orgs//memberships//activate │ │ │ ─────────────────────────>│ activate_request() │ │ │ "turn on ZeroTier" ├──> creates ActivationSession (TTL=8h) │ │ ├──> sets request.active=True │ │ │ ├──> _authorize_in_zerotier() │ │ │ │ └──> authorizes member ───────────────────────────────>│ │ │ │ │ │ <── 200 { active: true, session: {...} } │ │ ``` ### Endpoints | Method | Path | Action | |---|---|---| | `POST` | `/organizations//devices` | Register a device (prerequisite) | | `POST` | `/organizations//approvals` | Request network access | | `POST` | `/organizations//approvals//approve` | Admin approves | | `POST` | `/organizations//memberships//activate` | **Turn on ZeroTier** | | `POST` | `/organizations//memberships/activate-all` | Bulk-activate all approved | | `POST` | `/organizations//memberships//deactivate` | Turn off ZeroTier | | `POST` | `/organizations//devices//join-network/` | Direct join (open networks) | | `POST` | `/organizations//kill-switch` | Emergency deactivation | ## Session expiry and audit When the `ActivationSession` TTL expires, the reconciliation worker handles it: ``` Reconciliation worker (runs every 2 min) │ ├── reconcile_expired_activations() │ └── for each expired ActivationSession: │ ├── _deauthorize_in_zerotier() ───> ZT controller de-authorizes member │ ├── sets request.active = False │ └── logs audit event │ └── reconcile_all() └── for each network: ├── sync ZeroTierMembership cache └── detect/repair drift (portal vs controller state mismatch) ``` Every authorization state change is audited: | Event | When | |---|---| | `zt.approval.requested` | User requests access | | `zt.approval.granted` | Manager approves/assigns | | `zt.approval.rejected` | Manager rejects | | `zt.approval.revoked` | Manager revokes | | `zt.membership.activated` | User activates (session created) | | `zt.membership.deactivated` | User deactivates (session ended) | | `zt.member.authorized` | ZT controller authorize call succeeds | | `zt.member.deauthorized` | ZT controller de-authorize call succeeds | | `zt.activation.expired` | Session expired by reconciliation worker | | `zt.kill_switch.activated` | Admin triggers kill switch | All audit entries are stored in the `audit_logs` table with `organization_id`, `user_id`, `resource_type`, `resource_id`, `ip_address`, and `extra_data` (JSON) for full traceability. ## Key source files | File | Purpose | |---|---| | `gatehouse_app/models/zerotier/network_access_request.py` | `NetworkAccessRequest` model | | `gatehouse_app/models/zerotier/activation_session.py` | `ActivationSession` model | | `gatehouse_app/models/zerotier/zerotier_membership.py` | `ZeroTierMembership` model | | `gatehouse_app/services/network_access_service.py` | Core business logic | | `gatehouse_app/services/zerotier_reconciliation_service.py` | Reconciliation worker logic | | `gatehouse_app/api/v1/zerotier.py` | API endpoints | | `gatehouse_app/utils/constants.py` | Audit action enums | | `gatehouse_app/jobs/zerotier_reconciliation_job.py` | Scheduled job entry point | | `migrations/versions/merge_approval_membership_tables.py` | Schema migration |