feat: allow admins to bypass approval flow when joining networks

This commit is contained in:
Ubuntu
2026-05-07 20:04:08 +00:00
parent 32d517ea08
commit d100fdff3b
34 changed files with 2523 additions and 1637 deletions
@@ -7,16 +7,14 @@ from datetime import datetime, timezone
from gatehouse_app.extensions import db
from gatehouse_app.models import (
Device,
DeviceNetworkMembership,
NetworkAccessRequest,
ActivationSession,
ZeroTierMembership,
PortalNetwork,
UserNetworkApproval,
)
from gatehouse_app.services import zerotier_api_service as zt
from gatehouse_app.utils.constants import (
ActivationEndReason,
MembershipState,
ApprovalState,
)
@@ -45,7 +43,7 @@ def reconcile_expired_activations() -> int:
except Exception as exc:
logger.error(
f"[Reconciliation] Failed to expire session {session.id} "
f"(user={session.user_id} membership={session.device_network_membership_id}): {exc}",
f"(user={session.user_id} request={session.network_access_request_id}): {exc}",
exc_info=True,
)
@@ -104,9 +102,9 @@ def reconcile_network(portal_network_id: str) -> dict:
# Get our portal memberships for this network
our_memberships = {
m.device.node_id: m
for m in DeviceNetworkMembership.query.filter(
DeviceNetworkMembership.portal_network_id == portal_network_id,
DeviceNetworkMembership.deleted_at.is_(None),
for m in NetworkAccessRequest.query.filter(
NetworkAccessRequest.portal_network_id == portal_network_id,
NetworkAccessRequest.deleted_at.is_(None),
).all()
if m.device and m.device.deleted_at is None
}
@@ -124,7 +122,7 @@ def reconcile_network(portal_network_id: str) -> dict:
# Member not seen in ZT yet — could be freshly joined or never connected
logger.debug(
f"[Reconciliation] {network_label}: node {node_id} "
f"(device={device.display_name!r}, state={membership.state}) not yet seen in ZT controller."
f"(device={device.display_name!r}, active={membership.active}) not yet seen in ZT controller."
)
continue
@@ -134,11 +132,11 @@ def reconcile_network(portal_network_id: str) -> dict:
_sync_zt_membership(membership, zt_member)
# Sync authorization state
if membership.state == MembershipState.ACTIVE_AUTHORIZED:
if membership.active:
if not zt_member.is_authorized:
# Portal says active but ZT disagrees — drift, re-authorize
logger.warning(
f"[Reconciliation] {network_label}: DRIFT detected — portal=ACTIVE_AUTHORIZED "
f"[Reconciliation] {network_label}: DRIFT detected — portal=active "
f"but ZT says unauthorized for node {node_id} (device={device.display_name!r}). Re-authorizing."
)
try:
@@ -154,13 +152,13 @@ def reconcile_network(portal_network_id: str) -> dict:
)
else:
logger.debug(
f"[Reconciliation] {network_label}: node {node_id} — portal=ACTIVE_AUTHORIZED, ZT=authorized. OK."
f"[Reconciliation] {network_label}: node {node_id} — portal=active, ZT=authorized. OK."
)
else:
if zt_member.is_authorized:
# ZT says authorized but portal doesn't — could be manual override in ZT console
logger.warning(
f"[Reconciliation] {network_label}: DRIFT detected — portal state={membership.state} "
f"[Reconciliation] {network_label}: DRIFT detected — portal=inactive "
f"but ZT says authorized for node {node_id} (device={device.display_name!r}). Deauthorizing."
)
try:
@@ -177,7 +175,7 @@ def reconcile_network(portal_network_id: str) -> dict:
else:
logger.debug(
f"[Reconciliation] {network_label}: node {node_id}"
f"portal={membership.state}, ZT=unauthorized. OK."
f"portal=inactive, ZT=unauthorized. OK."
)
# Unknown ZT members not in our portal — log only, do not touch
@@ -261,11 +259,11 @@ def reconcile_deleted_memberships() -> dict:
"""Find soft-deleted memberships and hard-delete them after ZeroTier cleanup.
Only processes memberships whose ZeroTier members are already de-authorized
(the de-authorize step happened in revoke_membership_soft). This function
(the de-authorize step happened in revoke_request_soft). This function
removes the member from ZeroTier entirely and then hard-deletes the DB record.
"""
deleted = DeviceNetworkMembership.query.filter(
DeviceNetworkMembership.deleted_at.isnot(None),
deleted = NetworkAccessRequest.query.filter(
NetworkAccessRequest.deleted_at.isnot(None),
).all()
if not deleted:
@@ -328,7 +326,7 @@ def reconcile_deleted_memberships() -> dict:
return results
def _sync_zt_membership(membership: DeviceNetworkMembership, zt_member) -> None:
def _sync_zt_membership(membership: NetworkAccessRequest, zt_member) -> None:
"""Update the ZeroTierMembership cache record from a ZT API response."""
device = membership.device
network = membership.portal_network
@@ -347,7 +345,7 @@ def _sync_zt_membership(membership: DeviceNetworkMembership, zt_member) -> None:
)
zt_membership = ZeroTierMembership(
organization_id=membership.organization_id,
device_network_membership_id=membership.id,
network_access_request_id=membership.id,
zerotier_network_id=network.zerotier_network_id,
node_id=device.node_id,
)
@@ -377,10 +375,10 @@ def _sync_zt_membership(membership: DeviceNetworkMembership, zt_member) -> None:
logger.info(
f"[Reconciliation] First join seen for node {device.node_id} "
f"(device={device.display_name!r}, membership={membership.id}). "
f"State: {membership.state}{MembershipState.JOINED_DEAUTHORIZED}"
f"Setting join_seen=True, active=False"
)
membership.join_seen = True
membership.state = MembershipState.JOINED_DEAUTHORIZED
membership.active = False
membership.save()
else:
logger.debug(
@@ -397,23 +395,22 @@ def _expire_session(session: ActivationSession) -> None:
logger.info(
f"[Reconciliation] Expiring activation session {session.id} "
f"(user={session.user_id}, membership={session.device_network_membership_id}, "
f"(user={session.user_id}, request={session.network_access_request_id}, "
f"expired_at={session.expires_at.isoformat()})."
)
membership = DeviceNetworkMembership.query.get(session.device_network_membership_id)
if not membership:
request = NetworkAccessRequest.query.get(session.network_access_request_id)
if not request:
logger.warning(
f"[Reconciliation] Session {session.id}: membership "
f"{session.device_network_membership_id} not found — skipping ZT deauth."
f"[Reconciliation] Session {session.id}: request "
f"{session.network_access_request_id} not found — skipping ZT deauth."
)
else:
membership.state = MembershipState.ACTIVATION_EXPIRED
membership.currently_authorized = False
membership.save()
request.active = False
request.save()
device = Device.query.get(membership.device_id)
network = PortalNetwork.query.get(membership.portal_network_id)
device = Device.query.get(request.device_id)
network = PortalNetwork.query.get(request.portal_network_id)
if device and network:
network_label = f"{network.name} ({network.zerotier_network_id})"
try:
@@ -449,8 +446,8 @@ def _expire_session(session: ActivationSession) -> None:
else:
logger.warning(
f"[Reconciliation] Session {session.id}: missing "
f"{'device' if not device else 'network'} for membership "
f"{membership.id} — ZT deauth skipped."
f"{'device' if not device else 'network'} for request "
f"{request.id} — ZT deauth skipped."
)
from gatehouse_app.services.audit_service import AuditService
@@ -460,7 +457,7 @@ def _expire_session(session: ActivationSession) -> None:
organization_id=session.organization_id,
resource_type="activation_session",
resource_id=session.id,
metadata={"membership_id": session.device_network_membership_id},
metadata={"request_id": session.network_access_request_id},
description="Activation session expired",
success=True,
)