diff --git a/server-tools/enterprise-lighthouse/lighthouse.py b/server-tools/enterprise-lighthouse/lighthouse.py index 0095d8c..dc9834d 100644 --- a/server-tools/enterprise-lighthouse/lighthouse.py +++ b/server-tools/enterprise-lighthouse/lighthouse.py @@ -23,6 +23,8 @@ DOMAINS = { "DOMAIN-ZS": "零感域", "DOMAIN-MAIN": "光湖主域", "DOMAIN-SUB": "光湖分域", "DOMAIN-ZERO": "光湖零域", "DOMAIN-FIFTH": "第五域", } +ENTERPRISE_MANAGED_DOMAINS = {"DOMAIN-ZS", "DOMAIN-MAIN", "DOMAIN-SUB", "DOMAIN-ZERO"} +EXTERNAL_FOUNDATION_DOMAINS = {"DOMAIN-FIFTH"} def now(): @@ -53,7 +55,8 @@ def connection(): ); """) for domain_id, name in DOMAINS.items(): - db.execute("INSERT OR IGNORE INTO domains VALUES (?, ?, 'PENDING_ENTRY_NODE', ?)", (domain_id, name, now())) + state = "EXTERNAL_PRIVATE_FOUNDATION" if domain_id in EXTERNAL_FOUNDATION_DOMAINS else "PENDING_ENTRY_NODE" + db.execute("INSERT OR IGNORE INTO domains VALUES (?, ?, ?, ?)", (domain_id, name, state, now())) db.commit() return db @@ -123,6 +126,8 @@ class Handler(BaseHTTPRequestHandler): required = ("human_name", "email", "server_ip", "domain_id") if any(not payload.get(field) for field in required) or payload["domain_id"] not in DOMAINS or not valid_email(payload["email"]): return self.respond(400, {"ok": False, "error": "human_name, valid email, server_ip, and known domain_id are required"}) + if payload["domain_id"] not in ENTERPRISE_MANAGED_DOMAINS: + return self.respond(403, {"ok": False, "error": "this domain is not managed by the enterprise lighthouse; a separate explicit sovereign authorization is required"}) try: ip_address(payload["server_ip"]) except ValueError: @@ -146,8 +151,8 @@ class Handler(BaseHTTPRequestHandler): required = ("id", "domain_id", "display_name", "server_ip") if any(not isinstance(payload.get(field), str) or not payload[field].strip() for field in required): return self.respond(400, {"ok": False, "error": "id, domain_id, display_name, and server_ip are required"}) - if payload["domain_id"] not in DOMAINS: - return self.respond(400, {"ok": False, "error": "domain_id is not registered"}) + if payload["domain_id"] not in ENTERPRISE_MANAGED_DOMAINS: + return self.respond(403, {"ok": False, "error": "this domain cannot be registered by the enterprise lighthouse"}) try: ip_address(payload["server_ip"]) except ValueError: @@ -162,6 +167,20 @@ class Handler(BaseHTTPRequestHandler): db.execute("UPDATE domains SET state='ENTRY_NODE_CONNECTED' WHERE id=?", (payload["domain_id"],)) audit(db, "node_bootstrap_connected", {"node_id": node_id, "domain_id": payload["domain_id"], "mode": "manual_verified_routing_only"}) return self.respond(201, {"ok": True, "node_id": node_id, "state": "CONNECTED_PENDING_CONNECTOR", "execution": "disabled until GLSV connector enrollment and human authorization"}) + if self.path == "/v1/nodes/revoke": + node_id = payload.get("node_id") + if not isinstance(node_id, str) or not node_id.strip(): + return self.respond(400, {"ok": False, "error": "node_id is required"}) + node = db.execute("SELECT id, domain_id, state FROM nodes WHERE id=?", (node_id.strip(),)).fetchone() + if not node: + return self.respond(404, {"ok": False, "error": "node is not registered"}) + if node["state"] == "ACTIVE": + return self.respond(409, {"ok": False, "error": "ACTIVE nodes require a separate migration or retirement procedure"}) + db.execute("DELETE FROM nodes WHERE id=?", (node["id"],)) + if node["domain_id"] in EXTERNAL_FOUNDATION_DOMAINS: + db.execute("UPDATE domains SET state='EXTERNAL_PRIVATE_FOUNDATION' WHERE id=?", (node["domain_id"],)) + audit(db, "node_revoked", {"node_id": node["id"], "domain_id": node["domain_id"], "reason": "administrative revocation"}) + return self.respond(200, {"ok": True, "node_id": node["id"], "state": "REVOKED", "execution": "disabled"}) if self.path == "/v1/preflight": action, node_id = payload.get("action"), payload.get("target_node_id") if action not in FIXED_ACTIONS or not node_id: diff --git a/server-tools/enterprise-lighthouse/test_lighthouse.py b/server-tools/enterprise-lighthouse/test_lighthouse.py index 2d855df..5bc05d7 100644 --- a/server-tools/enterprise-lighthouse/test_lighthouse.py +++ b/server-tools/enterprise-lighthouse/test_lighthouse.py @@ -20,12 +20,16 @@ with tempfile.TemporaryDirectory() as temp: except OSError: time.sleep(.1) else: raise AssertionError("server did not start") - request = urllib.request.Request("http://127.0.0.1:48031/v1/intakes", data=json.dumps({"human_name":"Test","email":"test@example.invalid","server_ip":"203.0.113.8","domain_id":"DOMAIN-FIFTH"}).encode(), method="POST", headers={"X-Lighthouse-Admin-Token":"test-token"}) + request = urllib.request.Request("http://127.0.0.1:48031/v1/intakes", data=json.dumps({"human_name":"Test","email":"test@example.invalid","server_ip":"203.0.113.8","domain_id":"DOMAIN-ZS"}).encode(), method="POST", headers={"X-Lighthouse-Admin-Token":"test-token"}) assert json.load(urllib.request.urlopen(request))["state"] == "PENDING_REVIEW" - bootstrap = urllib.request.Request("http://127.0.0.1:48031/v1/nodes/bootstrap", data=json.dumps({"id":"NODE-TEST-001","domain_id":"DOMAIN-FIFTH","display_name":"Test entry","server_ip":"203.0.113.8"}).encode(), method="POST", headers={"X-Lighthouse-Admin-Token":"test-token"}) + bootstrap = urllib.request.Request("http://127.0.0.1:48031/v1/nodes/bootstrap", data=json.dumps({"id":"NODE-TEST-001","domain_id":"DOMAIN-ZS","display_name":"Test entry","server_ip":"203.0.113.8"}).encode(), method="POST", headers={"X-Lighthouse-Admin-Token":"test-token"}) assert json.load(urllib.request.urlopen(bootstrap))["state"] == "CONNECTED_PENDING_CONNECTOR" preflight = urllib.request.Request("http://127.0.0.1:48031/v1/preflight", data=json.dumps({"action":"health_check","target_node_id":"NODE-TEST-001"}).encode(), method="POST", headers={"X-Lighthouse-Admin-Token":"test-token"}) assert json.load(urllib.request.urlopen(preflight))["decision"] == "REJECT" + fifth = urllib.request.Request("http://127.0.0.1:48031/v1/nodes/bootstrap", data=json.dumps({"id":"NODE-FIFTH-001","domain_id":"DOMAIN-FIFTH","display_name":"Not allowed","server_ip":"203.0.113.8"}).encode(), method="POST", headers={"X-Lighthouse-Admin-Token":"test-token"}) + try: urllib.request.urlopen(fifth) + except urllib.error.HTTPError as error: assert error.code == 403 + else: raise AssertionError("private fifth domain was accepted by enterprise lighthouse") bad = urllib.request.Request("http://127.0.0.1:48031/v1/intakes", data=b'{"cmd":"rm -rf /"}', method="POST", headers={"X-Lighthouse-Admin-Token":"test-token"}) try: urllib.request.urlopen(bad) except urllib.error.HTTPError as error: assert error.code == 400