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:
@@ -0,0 +1,107 @@
|
||||
"""Activation session model — temporary activation window for a device membership."""
|
||||
|
||||
from gatehouse_app.extensions import db
|
||||
from gatehouse_app.models.base import BaseModel
|
||||
from gatehouse_app.utils.constants import ActivationEndReason
|
||||
|
||||
|
||||
class ActivationSession(BaseModel):
|
||||
"""A temporary activation window for an already-approved device membership.
|
||||
|
||||
Created when a user re-authenticates in the portal and wants to activate
|
||||
an approved device on a network. Has a fixed lifetime (e.g. 8 hours).
|
||||
When it expires the membership is de-authorized in ZeroTier but the
|
||||
underlying approval record is untouched.
|
||||
|
||||
Attributes:
|
||||
organization_id: FK to the organization
|
||||
user_id: FK to the user who owns the session
|
||||
device_network_membership_id: FK to the related membership
|
||||
authenticated_at: When the user re-authenticated to start this session
|
||||
expires_at: When the activation window ends
|
||||
ended_at: When the session was explicitly ended (null if still active)
|
||||
end_reason: Why the session ended (expired, logout, kill_switch, etc.)
|
||||
created_by: FK to the user who triggered activation (usually same as user_id)
|
||||
"""
|
||||
|
||||
__tablename__ = "activation_sessions"
|
||||
|
||||
organization_id = db.Column(
|
||||
db.String(36),
|
||||
db.ForeignKey("organizations.id"),
|
||||
nullable=False,
|
||||
index=True,
|
||||
)
|
||||
user_id = db.Column(
|
||||
db.String(36),
|
||||
db.ForeignKey("users.id"),
|
||||
nullable=False,
|
||||
index=True,
|
||||
)
|
||||
device_network_membership_id = db.Column(
|
||||
db.String(36),
|
||||
db.ForeignKey("device_network_memberships.id"),
|
||||
nullable=False,
|
||||
index=True,
|
||||
)
|
||||
authenticated_at = db.Column(
|
||||
db.DateTime(timezone=True),
|
||||
nullable=False,
|
||||
)
|
||||
expires_at = db.Column(
|
||||
db.DateTime(timezone=True),
|
||||
nullable=False,
|
||||
)
|
||||
ended_at = db.Column(db.DateTime(timezone=True), nullable=True)
|
||||
end_reason = db.Column(
|
||||
db.Enum(ActivationEndReason, name="activation_end_reason"),
|
||||
nullable=True,
|
||||
)
|
||||
created_by = db.Column(
|
||||
db.String(36),
|
||||
db.ForeignKey("users.id"),
|
||||
nullable=False,
|
||||
)
|
||||
|
||||
# Relationships
|
||||
organization = db.relationship("Organization", backref="activation_sessions")
|
||||
user = db.relationship(
|
||||
"User",
|
||||
foreign_keys=[user_id],
|
||||
backref="activation_sessions",
|
||||
)
|
||||
created_by_user = db.relationship(
|
||||
"User",
|
||||
foreign_keys=[created_by],
|
||||
backref="created_activation_sessions",
|
||||
)
|
||||
membership = db.relationship(
|
||||
"DeviceNetworkMembership",
|
||||
back_populates="activation_sessions",
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
return (
|
||||
f"<ActivationSession membership={self.device_network_membership_id} "
|
||||
f"expires={self.expires_at}>"
|
||||
)
|
||||
|
||||
@property
|
||||
def is_expired(self) -> bool:
|
||||
from datetime import datetime, timezone
|
||||
now = datetime.now(timezone.utc)
|
||||
exp = self.expires_at
|
||||
if exp.tzinfo is None:
|
||||
exp = exp.replace(tzinfo=timezone.utc)
|
||||
return now > exp
|
||||
|
||||
@property
|
||||
def is_active(self) -> bool:
|
||||
return self.ended_at is None and not self.is_expired
|
||||
|
||||
def to_dict(self, exclude=None):
|
||||
exclude = exclude or []
|
||||
data = super().to_dict(exclude=exclude)
|
||||
data["is_expired"] = self.is_expired
|
||||
data["is_active"] = self.is_active
|
||||
return data
|
||||
Reference in New Issue
Block a user