Feat(Fix): Multi-Tenant Zerotier Org Setups
Imports Network From Zerotier Async Emails Migration guardrails Admin to see all approvals states
This commit is contained in:
@@ -1,7 +1,11 @@
|
||||
"""ZeroTier API service — thin Flask adapter around the ZeroTierClient SDK.
|
||||
|
||||
Reads configuration from app config and translates SDK exceptions to
|
||||
Secuird typed exceptions.
|
||||
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
|
||||
@@ -19,97 +23,147 @@ from gatehouse_app.utils.zerotier_client import (
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _get_client(app=None) -> ZeroTierClient:
|
||||
"""Build a ZeroTierClient from current app config."""
|
||||
from flask import current_app
|
||||
def _get_client(organization_id: Optional[str] = None, app=None) -> ZeroTierClient:
|
||||
"""Build a ZeroTierClient using the organization's stored ZeroTier credentials.
|
||||
|
||||
app = app or current_app
|
||||
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_str = app.config.get("ZEROTIER_API_MODE", "controller")
|
||||
mode = APIMode.CENTRAL if mode_str == "central" else APIMode.CONTROLLER
|
||||
|
||||
return ZeroTierClient(
|
||||
api_token=app.config.get("ZEROTIER_API_TOKEN", ""),
|
||||
base_url=app.config.get("ZEROTIER_API_URL", "http://localhost:9993"),
|
||||
mode=mode,
|
||||
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() -> dict:
|
||||
|
||||
def get_status(organization_id: Optional[str] = None) -> dict:
|
||||
"""Verify connectivity to the ZeroTier controller."""
|
||||
client = _get_client()
|
||||
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():
|
||||
def list_networks(organization_id: Optional[str] = None):
|
||||
"""List all networks accessible to the configured token."""
|
||||
client = _get_client()
|
||||
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):
|
||||
def get_network(network_id: str, organization_id: Optional[str] = None):
|
||||
"""Fetch a single network by ID."""
|
||||
client = _get_client()
|
||||
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):
|
||||
def list_members(network_id: str, organization_id: Optional[str] = None):
|
||||
"""List all members on a network."""
|
||||
client = _get_client()
|
||||
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):
|
||||
def get_member(network_id: str, node_id: str, organization_id: Optional[str] = None):
|
||||
"""Fetch a single member on a network."""
|
||||
client = _get_client()
|
||||
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):
|
||||
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()
|
||||
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):
|
||||
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()
|
||||
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):
|
||||
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()
|
||||
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):
|
||||
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()
|
||||
client = _get_client(organization_id)
|
||||
try:
|
||||
return client.delete_member(network_id, node_id)
|
||||
except SDKZeroTierAPIError as exc:
|
||||
|
||||
Reference in New Issue
Block a user