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
@@ -561,6 +561,52 @@ def kill_switch(
return count
def kill_switch_network(
portal_network_id: str,
organization_id: str,
admin_user_id: str,
) -> int:
"""Deactivate all active memberships on a network across all users."""
requests = NetworkAccessRequest.query.filter(
NetworkAccessRequest.portal_network_id == portal_network_id,
NetworkAccessRequest.organization_id == organization_id,
NetworkAccessRequest.active == True,
NetworkAccessRequest.deleted_at.is_(None),
).all()
count = 0
for r in requests:
_end_active_session(r, reason=ActivationEndReason.KILL_SWITCH)
device = Device.query.get(r.device_id)
network = PortalNetwork.query.get(r.portal_network_id)
if device and network:
try:
zt.deauthorize_member(network.zerotier_network_id, device.node_id,
organization_id=r.organization_id)
except Exception as exc:
logger.warning(f"[kill_switch_network] Could not deauthorize {device.node_id}: {exc}")
r.active = False
if r.status == ApprovalState.APPROVED:
r.status = ApprovalState.SUSPENDED
r.save()
count += 1
AuditService.log_action(
action=AuditAction.ZT_NETWORK_KILL_SWITCH,
user_id=admin_user_id,
organization_id=organization_id,
resource_type="portal_network",
resource_id=portal_network_id,
metadata={"affected_count": count},
description=f"Network kill switch activated: {count} requests deactivated on network {portal_network_id}",
success=True,
)
return count
# ── Helpers ────────────────────────────────────────────────────────────────────
@@ -799,6 +845,66 @@ def join_network_for_device(
return request
def admin_end_session(
session_id: str,
admin_user_id: str,
) -> ActivationSession:
"""End a specific activation session (admin only).
Ends the session, deauthorizes the device in ZeroTier, and marks the
associated network access request as inactive. Does NOT change the
request's approval status — the user keeps their approval and can
re-authenticate without needing re-approval.
"""
session = ActivationSession.query.filter(
ActivationSession.id == session_id,
ActivationSession.deleted_at.is_(None),
).first()
if not session:
raise ApprovalNotFoundError(f"Session {session_id} not found.")
if session.ended_at:
raise ValidationError("Session already ended.")
# End the session with ADMIN_ACTION reason
_end_session(session, ActivationEndReason.ADMIN_ACTION)
# Deactivate the associated request
if session.network_access_request_id:
request = NetworkAccessRequest.query.get(session.network_access_request_id)
if request and request.active:
request.active = False
request.save()
# Deauthorize in ZeroTier
device = Device.query.get(request.device_id)
network = PortalNetwork.query.get(request.portal_network_id)
if device and network:
_deauthorize_in_zerotier(
device.node_id,
network.zerotier_network_id,
organization_id=request.organization_id,
)
AuditService.log_action(
action=AuditAction.ZT_SESSION_ENDED,
user_id=admin_user_id,
organization_id=session.organization_id,
resource_type="activation_session",
resource_id=session.id,
metadata={
"target_user_id": session.user_id,
"end_reason": ActivationEndReason.ADMIN_ACTION.value,
"network_access_request_id": session.network_access_request_id,
},
description=f"Admin terminated session for user {session.user_id}",
success=True,
)
return session
# ── Admin membership management ────────────────────────────────────────────────