feat: add network-level kill switch endpoint

This commit is contained in:
2026-05-30 06:32:26 +00:00
parent fed72f8bcd
commit 2aad17f5e0
8 changed files with 460 additions and 1 deletions
+169
View File
@@ -0,0 +1,169 @@
# ZeroTier Kill Switch
## Overview
The kill-switch mechanism provides emergency deactivation of ZeroTier network access at three granularities: a single device membership, all memberships for a user, or all memberships on a network. All kill operations are **reversible** — they set `active=False` and (in most cases) `status=SUSPENDED` but do not delete records, so affected users can re-activate or re-authenticate.
## Three Kill Operations
| Granularity | Endpoint | Admin-only | Behavior |
|---|---|---|---|
| **Device X on network Y** | `POST /orgs/<id>/memberships/<id>/deactivate` | No (owner can self-deactivate) | Sets `active=False`. Status stays APPROVED. |
| **All devices for a user** | `POST /orgs/<id>/kill-switch` | Yes | Sets `active=False`, `status=SUSPENDED` for every active membership in the org (optionally filtered to specific networks). |
| **All devices on a network** | `POST /orgs/<id>/networks/<id>/kill-switch` | Yes | Sets `active=False`, `status=SUSPENDED` for every active membership on the network, across all users. |
## Detailed Endpoint Reference
### 1. Kill a Single Membership (Device + Network)
```
POST /api/v1/organizations/<org_id>/memberships/<membership_id>/deactivate
```
**Auth:** `@login_required`, `@full_access_required`
**Admin override:** Admins can deactivate any membership; non-admins can only deactivate their own.
**Request body:** None
**Response (200):**
```json
{
"success": true,
"data": {
"request": {
"id": "...",
"active": false,
"status": "approved",
...
}
},
"message": "Request deactivated successfully"
}
```
**Behavior:**
- Ends the active `ActivationSession` with reason `manual_revoke`
- De-authorizes the device node in the ZeroTier controller
- Sets `request.active = False` (status **unchanged** — stays `approved`)
- Logs `zt.membership.deactivated` audit event
**Re-activation:** The user can re-activate via `POST /memberships/<id>/activate` or `POST /memberships/activate-all`.
---
### 2. Kill All Devices for a User
```
POST /api/v1/organizations/<org_id>/kill-switch
```
**Auth:** `@login_required`, `@require_admin`, `@full_access_required`
**Request body:**
```json
{
"target_user_id": "uuid-of-user-to-kill",
"scope": "organization",
"network_ids": ["uuid-of-network-1", "uuid-of-network-2"],
"reason": "Security incident — force deactivation"
}
```
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
| `target_user_id` | string (UUID) | yes | — | The user to deactivate |
| `scope` | string | no | `"organization"` | `"organization"` (all networks) or `"selected_networks"` |
| `network_ids` | array of UUIDs | no | `null` | Required when scope is `selected_networks` |
| `reason` | string | no | `null` | Max 500 chars |
**Response (200):**
```json
{
"success": true,
"data": {
"affected_count": 3
},
"message": "Kill switch triggered successfully"
}
```
**Behavior:**
- Queries all active, non-deleted `NetworkAccessRequest` rows for the target user
- For each: ends session (reason `kill_switch`), de-authorizes in ZT
- Sets `active = False`, **and** sets `status = SUSPENDED` if currently `APPROVED`
- Logs `zt.kill_switch.activated` audit event with `affected_count` and `scope`
**Re-activation:** The user's memberships are in `SUSPENDED` state. An admin must explicitly re-approve (change status back to `APPROVED`) before the user can re-activate.
---
### 3. Kill All Devices on a Network
```
POST /api/v1/organizations/<org_id>/networks/<network_id>/kill-switch
```
**Auth:** `@login_required`, `@require_admin`, `@full_access_required`
**Request body:**
```json
{
"reason": "Network compromised — emergency deactivation"
}
```
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
| `reason` | string | no | `null` | Max 500 chars |
**Response (200):**
```json
{
"success": true,
"data": {
"affected_count": 12
},
"message": "Network kill switch triggered successfully"
}
```
**Behavior:**
- Queries all active, non-deleted `NetworkAccessRequest` rows for the network, **regardless of user**
- For each: ends session (reason `kill_switch`), de-authorizes in ZT
- Sets `active = False`, and `status = SUSPENDED` if currently `APPROVED`
- Logs `zt.network_kill_switch.activated` audit event with `affected_count`
**Re-activation:** Same as user kill switch — each affected membership is `SUSPENDED` and needs admin re-approval.
## Comparison: Deactivation vs. Deletion
| Operation | active | status | deleted_at | DB row | Reversible? |
|---|---|---|---|---|---|
| `POST /memberships/<id>/deactivate` | `false` | unchanged | `null` | preserved | Yes — re-activate |
| `POST /kill-switch` (user) | `false` | `suspended` | `null` | preserved | Yes — admin re-approve |
| `POST /networks/<id>/kill-switch` | `false` | `suspended` | `null` | preserved | Yes — admin re-approve |
| `DELETE /memberships/<id>` (soft) | `false` | unchanged | set | preserved | Partial — depends on join logic |
| `DELETE /admin/memberships/<id>` | `false` | — | — | **hard-deleted** | No |
## Audit Events
| Event | Trigger |
|---|---|
| `zt.membership.deactivated` | Single membership deactivated (endpoint #1) |
| `zt.kill_switch.activated` | User kill switch triggered (endpoint #2) |
| `zt.network_kill_switch.activated` | Network kill switch triggered (endpoint #3) |
All audit entries include `organization_id`, `user_id` (the actor), `resource_type`, `resource_id`, and `metadata` (affected count, scope, network IDs).
## Key Source Files
| File | Purpose |
|---|---|
| `gatehouse_app/api/v1/zerotier.py` | Route handlers for all three endpoints |
| `gatehouse_app/services/network_access_service.py` | `deactivate_request()`, `kill_switch()`, `kill_switch_network()` |
| `gatehouse_app/services/zerotier_api_service.py` | `deauthorize_member()` — ZT controller call |
| `gatehouse_app/utils/constants.py` | `AuditAction` and `KillSwitchScope` enums |
| `gatehouse_app/models/zerotier/network_access_request.py` | `NetworkAccessRequest` model |
| `gatehouse_app/models/zerotier/activation_session.py` | `ActivationSession` model |
| `gatehouse_app/models/zerotier/kill_switch_event.py` | `KillSwitchEvent` model |
| `tests/integration/test_zerotier.py` | Integration tests in `TestZeroTierMembership` |