furtka-gateway/control_plane/accounts.py
Robert Syrnicki 3a9d18fcd5 Scaffold gateway stack and single-tenant control plane
wg-easy + Traefik docker-compose stack (Phase 1) plus a stdlib-only
control-plane API for box registration, WireGuard peer provisioning via
wg-easy, and per-box route publish/unpublish backed by Traefik's file
provider (Phase 2, single-tenant mode). SQLite holds accounts/boxes/routes
so a later multi-tenant shared instance is the same schema with more rows,
not a reshape.

wg-easy's actual REST API was verified against its source rather than
assumed: it has no bearer-token auth (session-cookie login via
POST /api/auth/password) and no way to accept an externally-generated
public key (it always mints the keypair itself, private key included) —
both corrected from the original plan during implementation.
2026-08-24 11:50:25 +02:00

46 lines
1.6 KiB
Python

"""Account bootstrap/lookup.
Full multi-tenant account issuance (``POST /v1/accounts``, registration-token
rotation, per-account management) is Phase 4 — see the plan's phased
delivery. Phase 2 only needs the single implicit account a
``GATEWAY_MODE=single`` deployment bootstraps itself with at startup: its
``registration_token_hash``/``account_token_hash`` are left blank because
single-tenant auth for box registration goes through the operator-set
``GATEWAY_BOX_TOKEN`` env var instead (see api.py), not through this row.
"""
from __future__ import annotations
import sqlite3
from datetime import UTC, datetime
from control_plane.db import Database
SINGLE_TENANT_ACCOUNT_ID = "default"
def ensure_single_tenant_account(
db: Database, box_limit: int, route_limit_per_box: int
) -> str:
"""Idempotently create the one account a single-tenant gateway uses.
Returns the account id every registered box on this gateway belongs to.
"""
row = db.query_one("SELECT id FROM accounts WHERE id = ?", (SINGLE_TENANT_ACCOUNT_ID,))
if row is not None:
return SINGLE_TENANT_ACCOUNT_ID
now = datetime.now(UTC).isoformat()
db.execute(
"""
INSERT INTO accounts
(id, email, created_at, registration_token_hash, account_token_hash,
box_limit, route_limit_per_box)
VALUES (?, NULL, ?, '', '', ?, ?)
""",
(SINGLE_TENANT_ACCOUNT_ID, now, box_limit, route_limit_per_box),
)
return SINGLE_TENANT_ACCOUNT_ID
def get_account(db: Database, account_id: str) -> sqlite3.Row | None:
return db.query_one("SELECT * FROM accounts WHERE id = ?", (account_id,))