Files
JamesBhattarai 2b6f7e15af Feat(Fix): Multi-Tenant Zerotier Org Setups
Imports Network From Zerotier
Async Emails
Migration guardrails
Admin to see all approvals states
2026-03-31 12:33:56 +05:45

171 lines
6.5 KiB
Python

"""ZeroTier API service — thin Flask adapter around the ZeroTierClient SDK.
ZeroTier is managed exclusively at the organization level. Each organization
configures its own ZeroTier credentials (token, URL, mode) via the web UI
(ZeroTier Config page → stored in the organizations table).
Every call that interacts with ZeroTier must supply an organization_id so the correct org credentials
can be loaded from the database.
"""
import logging
from typing import Optional
from gatehouse_app.exceptions import ZeroTierAPIError
from gatehouse_app.utils.zerotier_client import (
APIMode,
ZeroTierAPIError as SDKZeroTierAPIError,
ZeroTierAuthError,
ZeroTierClient,
ZeroTierNotFoundError,
)
logger = logging.getLogger(__name__)
def _get_client(organization_id: Optional[str] = None, app=None) -> ZeroTierClient:
"""Build a ZeroTierClient using the organization's stored ZeroTier credentials.
Credentials are read exclusively from the organization record
(org.zt_api_token / org.zt_api_url / org.zt_api_mode).
Args:
organization_id: The org whose credentials should be used.
Required for any ZeroTier operation.
app: Flask app instance (defaults to current_app, only needed for
background tasks that run outside a request context).
Raises:
ZeroTierAPIError: If organization_id is missing, the org is not found,
or the org has incomplete ZeroTier credentials.
"""
if not organization_id:
raise ZeroTierAPIError(
"organization_id is required — ZeroTier credentials are managed "
"per-organization. Configure them via the ZeroTier Config page."
)
try:
from gatehouse_app.models.organization.organization import Organization
from gatehouse_app.extensions import db
org = db.session.get(Organization, organization_id)
except Exception as exc:
logger.error(f"[ZT] Failed to load org {organization_id} from DB: {exc}")
raise ZeroTierAPIError(
f"Could not load organization {organization_id}: {exc}"
) from exc
if not org:
raise ZeroTierAPIError(f"Organization {organization_id} not found.")
token: Optional[str] = org.zt_api_token or None
if not token:
raise ZeroTierAPIError(
f"Organization '{org.name}' has no ZeroTier credentials configured. "
"Go to Settings → ZeroTier Config to add a token, mode, and controller URL."
)
mode_str = (org.zt_api_mode or "").strip().lower()
if mode_str not in ("central", "controller"):
raise ZeroTierAPIError(
f"Organization '{org.name}' has no ZeroTier mode set. "
"Go to Settings → ZeroTier Config and select 'Central' or 'Controller'."
)
url: str = (org.zt_api_url or "").strip()
if not url:
raise ZeroTierAPIError(
f"Organization '{org.name}' has no ZeroTier controller/API URL set. "
"Go to Settings → ZeroTier Config and enter the URL for your ZeroTier "
"controller (e.g. http://host:9993) or Central API."
)
mode = APIMode.CENTRAL if mode_str == "central" else APIMode.CONTROLLER
logger.debug(
f"[ZT] Client for org:{organization_id} mode={mode_str} url={url}"
)
return ZeroTierClient(api_token=token, base_url=url, mode=mode)
def get_status(organization_id: Optional[str] = None) -> dict:
"""Verify connectivity to the ZeroTier controller."""
client = _get_client(organization_id)
try:
return client.get_status()
except SDKZeroTierAPIError as exc:
raise ZeroTierAPIError(str(exc), status_code=exc.status_code) from exc
def list_networks(organization_id: Optional[str] = None):
"""List all networks accessible to the configured token."""
client = _get_client(organization_id)
try:
return client.list_networks()
except SDKZeroTierAPIError as exc:
raise ZeroTierAPIError(str(exc), status_code=exc.status_code) from exc
def get_network(network_id: str, organization_id: Optional[str] = None):
"""Fetch a single network by ID."""
client = _get_client(organization_id)
try:
return client.get_network(network_id)
except SDKZeroTierAPIError as exc:
raise ZeroTierAPIError(str(exc), status_code=exc.status_code) from exc
def list_members(network_id: str, organization_id: Optional[str] = None):
"""List all members on a network."""
client = _get_client(organization_id)
try:
return client.list_members(network_id)
except SDKZeroTierAPIError as exc:
raise ZeroTierAPIError(str(exc), status_code=exc.status_code) from exc
def get_member(network_id: str, node_id: str, organization_id: Optional[str] = None):
"""Fetch a single member on a network."""
client = _get_client(organization_id)
try:
return client.get_member(network_id, node_id)
except SDKZeroTierAPIError as exc:
raise ZeroTierAPIError(str(exc), status_code=exc.status_code) from exc
def authorize_member(network_id: str, node_id: str, organization_id: Optional[str] = None):
"""Authorize a member on a network. Returns updated member."""
client = _get_client(organization_id)
try:
return client.authorize_member(network_id, node_id)
except SDKZeroTierAPIError as exc:
raise ZeroTierAPIError(str(exc), status_code=exc.status_code) from exc
def deauthorize_member(network_id: str, node_id: str, organization_id: Optional[str] = None):
"""De-authorize a member on a network. Returns updated member."""
client = _get_client(organization_id)
try:
return client.deauthorize_member(network_id, node_id)
except SDKZeroTierAPIError as exc:
raise ZeroTierAPIError(str(exc), status_code=exc.status_code) from exc
def add_member(network_id: str, node_id: str, authorized: bool = False, organization_id: Optional[str] = None):
"""Manually add/pre-provision a member on a network."""
client = _get_client(organization_id)
try:
return client.add_member(network_id, node_id, authorized=authorized)
except SDKZeroTierAPIError as exc:
raise ZeroTierAPIError(str(exc), status_code=exc.status_code) from exc
def delete_network_member(network_id: str, node_id: str, organization_id: Optional[str] = None):
"""Remove a member entirely from a ZeroTier network."""
client = _get_client(organization_id)
try:
return client.delete_member(network_id, node_id)
except SDKZeroTierAPIError as exc:
raise ZeroTierAPIError(str(exc), status_code=exc.status_code) from exc