"""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