Updated ZeroTier network membership flow and logic
This commit is contained in:
@@ -0,0 +1,165 @@
|
||||
# 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 |
|
||||
Reference in New Issue
Block a user