166 lines
8.9 KiB
Markdown
166 lines
8.9 KiB
Markdown
|
|
# 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/<id>/approvals │ │ │
|
||
|
|
├───────────────────────────>│ request_access() │ │
|
||
|
|
│ ├──> creates NetworkAccessRequest (status=PENDING) │
|
||
|
|
│ ├──> _ensure_zerotier_member() │
|
||
|
|
│ │ └──> provisions member (de-authorized) ────────────────>│
|
||
|
|
│ │ │ │
|
||
|
|
│ <── 201 { status: pending } │ │
|
||
|
|
│ │ │ │
|
||
|
|
│ [Admin approves] │ │ │
|
||
|
|
│ POST /orgs/<id>/approvals │ │ │
|
||
|
|
│ /<id>/approve │ │ │
|
||
|
|
├───────────────────────────>│ approve_request() │ │
|
||
|
|
│ ├──> sets status=APPROVED │ │
|
||
|
|
│ <── 200 { status: approved } │ │
|
||
|
|
│ │ │ │
|
||
|
|
│ POST /orgs/<id>/memberships/<id>/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/<id>/devices` | Register a device (prerequisite) |
|
||
|
|
| `POST` | `/organizations/<id>/approvals` | Request network access |
|
||
|
|
| `POST` | `/organizations/<id>/approvals/<id>/approve` | Admin approves |
|
||
|
|
| `POST` | `/organizations/<id>/memberships/<id>/activate` | **Turn on ZeroTier** |
|
||
|
|
| `POST` | `/organizations/<id>/memberships/activate-all` | Bulk-activate all approved |
|
||
|
|
| `POST` | `/organizations/<id>/memberships/<id>/deactivate` | Turn off ZeroTier |
|
||
|
|
| `POST` | `/organizations/<id>/devices/<id>/join-network/<id>` | Direct join (open networks) |
|
||
|
|
| `POST` | `/organizations/<id>/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 |
|