feat(zerotier): add ZeroTier network governance module

Add comprehensive ZeroTier integration for managing network access:

- Portal networks: manager-created ZeroTier network bindings
- Device registration: user-owned ZeroTier node endpoints
- Approval workflows: request/approve/revoke network access
- Activation sessions: time-limited network authorization
- Kill switch: emergency access revocation
- Reconciliation job: sync portal state with ZeroTier controller

Includes ZeroTier client SDK supporting both Central and self-hosted
controller APIs, with full CRUD operations for networks and members.
This commit is contained in:
2026-03-20 21:50:20 +10:30
parent 49e724222f
commit 1789590167
27 changed files with 4862 additions and 4 deletions
+79
View File
@@ -0,0 +1,79 @@
"""Device model — a user-registered ZeroTier node endpoint."""
from gatehouse_app.extensions import db
from gatehouse_app.models.base import BaseModel
from gatehouse_app.utils.constants import DeviceStatus
class Device(BaseModel):
"""A user-owned endpoint identified by a ZeroTier Node ID.
A user registers their device in the portal using the 10-character ZeroTier
Node ID visible in the ZeroTier client. One active device record per
node_id at a time (unique constraint excludes soft-deleted duplicates).
Attributes:
user_id: FK to the owning user
organization_id: FK to the organization this device is registered in
node_id: 10-char hex ZeroTier node / device address
device_nickname: User-assigned friendly name
hostname: Device hostname reported by the client
asset_tag: Corporate asset tag if available
serial_number: Device serial number if available
status: active / inactive
"""
__tablename__ = "devices"
user_id = db.Column(
db.String(36),
db.ForeignKey("users.id"),
nullable=False,
index=True,
)
organization_id = db.Column(
db.String(36),
db.ForeignKey("organizations.id"),
nullable=False,
index=True,
)
node_id = db.Column(
db.String(10),
nullable=False,
index=True,
)
device_nickname = db.Column(db.String(255), nullable=True)
hostname = db.Column(db.String(255), nullable=True)
asset_tag = db.Column(db.String(255), nullable=True)
serial_number = db.Column(db.String(255), nullable=True)
status = db.Column(
db.Enum(DeviceStatus, name="device_status"),
default=DeviceStatus.ACTIVE,
nullable=False,
)
# Relationships
user = db.relationship("User", backref="devices")
organization = db.relationship("Organization", backref="devices")
memberships = db.relationship(
"DeviceNetworkMembership",
back_populates="device",
cascade="all, delete-orphan",
)
def __repr__(self):
return f"<Device {self.node_id} ({self.device_nickname or 'unnamed'})>"
@property
def display_name(self) -> str:
return self.device_nickname or self.hostname or self.node_id
def to_dict(self, exclude=None):
exclude = exclude or []
data = super().to_dict(exclude=exclude)
data["display_name"] = self.display_name
data["active_membership_count"] = sum(
1 for m in self.memberships
if m.state == "active_authorized" and m.deleted_at is None
)
return data