feat(enterprise): route four domains through lighthouse
This commit is contained in:
parent
1d2a8a2f63
commit
c058974bba
12 changed files with 707 additions and 20 deletions
|
|
@ -0,0 +1,308 @@
|
|||
#!/usr/bin/env python3
|
||||
"""HoloLake enterprise identity, relationship and responsibility receipt service.
|
||||
|
||||
The service binds only to loopback. Nginx exposes exact routes. Credentials are
|
||||
verified against the local Forgejo API and are never stored or logged.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
import hashlib
|
||||
import hmac
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sqlite3
|
||||
import time
|
||||
import urllib.error
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
import uuid
|
||||
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||
from pathlib import Path
|
||||
|
||||
BIND = os.environ.get("GH_ENTERPRISE_IDENTITY_BIND", "127.0.0.1")
|
||||
PORT = int(os.environ.get("GH_ENTERPRISE_IDENTITY_PORT", "8032"))
|
||||
DB_PATH = os.environ.get(
|
||||
"GH_ENTERPRISE_IDENTITY_DB",
|
||||
"/var/lib/guanghu-enterprise-identity/identity.sqlite3",
|
||||
)
|
||||
REGISTRY_PATH = os.environ.get(
|
||||
"GH_ENTERPRISE_IDENTITY_REGISTRY",
|
||||
"/etc/guanghu/enterprise-identity-registry.json",
|
||||
)
|
||||
FORGEJO_USER_API = os.environ.get(
|
||||
"GH_ENTERPRISE_FORGEJO_USER_API", "http://127.0.0.1:3341/api/v1/user"
|
||||
)
|
||||
RECEIPT_KEY = os.environ.get("GH_ENTERPRISE_RECEIPT_KEY", "")
|
||||
MAX_BODY = 16_384
|
||||
USERNAME = re.compile(r"^[A-Za-z0-9_-]{1,40}$")
|
||||
DECISIONS = {"ACCEPT", "REJECT", "DEFER", "ACCEPT_WITH_CHANGES"}
|
||||
|
||||
|
||||
def now() -> int:
|
||||
return int(time.time())
|
||||
|
||||
|
||||
def canonical(value: object) -> bytes:
|
||||
return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":")).encode()
|
||||
|
||||
|
||||
def load_registry() -> dict:
|
||||
registry = json.loads(Path(REGISTRY_PATH).read_text(encoding="utf-8"))
|
||||
if registry.get("schema") != "guanghu.enterprise-identity-registry/v1":
|
||||
raise ValueError("enterprise identity registry schema invalid")
|
||||
humans = registry.get("humans")
|
||||
if not isinstance(humans, list) or not humans:
|
||||
raise ValueError("enterprise identity registry is empty")
|
||||
numbers = [item.get("human_number") for item in humans]
|
||||
usernames = [item.get("username") for item in humans]
|
||||
if len(numbers) != len(set(numbers)) or len(usernames) != len(set(usernames)):
|
||||
raise ValueError("enterprise identity registry identities must be unique")
|
||||
for item in humans:
|
||||
if not USERNAME.fullmatch(str(item.get("username", ""))):
|
||||
raise ValueError("enterprise username invalid")
|
||||
for persona in item.get("personas", []):
|
||||
if persona.get("species") != "AGE" or str(persona.get("current_persona_identity", "")).startswith("AGE-"):
|
||||
raise ValueError("AGE is a species and cannot be used as a persona identity number")
|
||||
return registry
|
||||
|
||||
|
||||
def database() -> sqlite3.Connection:
|
||||
path = Path(DB_PATH)
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
db = sqlite3.connect(path)
|
||||
db.row_factory = sqlite3.Row
|
||||
db.executescript(
|
||||
"""
|
||||
PRAGMA journal_mode=WAL;
|
||||
PRAGMA foreign_keys=ON;
|
||||
CREATE TABLE IF NOT EXISTS relationship_receipts (
|
||||
receipt_id TEXT PRIMARY KEY,
|
||||
idempotency_key TEXT NOT NULL UNIQUE,
|
||||
human_number TEXT NOT NULL,
|
||||
username TEXT NOT NULL,
|
||||
registry_version TEXT NOT NULL,
|
||||
decision TEXT NOT NULL,
|
||||
observed_at INTEGER NOT NULL,
|
||||
receipt_hash TEXT NOT NULL,
|
||||
receipt_signature TEXT NOT NULL
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS responsibility_receipts (
|
||||
receipt_id TEXT PRIMARY KEY,
|
||||
idempotency_key TEXT NOT NULL UNIQUE,
|
||||
human_number TEXT NOT NULL,
|
||||
username TEXT NOT NULL,
|
||||
responsibility_domain TEXT NOT NULL,
|
||||
responsibility_version TEXT NOT NULL,
|
||||
decision TEXT NOT NULL,
|
||||
note TEXT NOT NULL,
|
||||
observed_at INTEGER NOT NULL,
|
||||
receipt_hash TEXT NOT NULL,
|
||||
receipt_signature TEXT NOT NULL
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS audit (
|
||||
sequence INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
observed_at INTEGER NOT NULL,
|
||||
kind TEXT NOT NULL,
|
||||
human_number_hash TEXT NOT NULL,
|
||||
receipt_id TEXT NOT NULL
|
||||
);
|
||||
"""
|
||||
)
|
||||
return db
|
||||
|
||||
|
||||
def find_human(registry: dict, number: str) -> dict | None:
|
||||
encoded = number.encode("utf-8")
|
||||
return next(
|
||||
(
|
||||
item
|
||||
for item in registry["humans"]
|
||||
if hmac.compare_digest(item["human_number"].encode("utf-8"), encoded)
|
||||
),
|
||||
None,
|
||||
)
|
||||
|
||||
|
||||
def parse_basic(header: str) -> tuple[str, str] | None:
|
||||
if not header.startswith("Basic "):
|
||||
return None
|
||||
try:
|
||||
decoded = base64.b64decode(header[6:], validate=True).decode("utf-8")
|
||||
username, password = decoded.split(":", 1)
|
||||
except (ValueError, UnicodeDecodeError):
|
||||
return None
|
||||
if not USERNAME.fullmatch(username) or not password or len(password) > 512:
|
||||
return None
|
||||
return username, password
|
||||
|
||||
|
||||
def verify_forgejo(username: str, password: str) -> bool:
|
||||
request = urllib.request.Request(FORGEJO_USER_API)
|
||||
credential = base64.b64encode(f"{username}:{password}".encode()).decode()
|
||||
request.add_header("Authorization", f"Basic {credential}")
|
||||
request.add_header("Accept", "application/json")
|
||||
try:
|
||||
with urllib.request.urlopen(request, timeout=10) as response:
|
||||
body = json.load(response)
|
||||
return response.status == 200 and hmac.compare_digest(str(body.get("login", "")), username)
|
||||
except (urllib.error.HTTPError, urllib.error.URLError, TimeoutError, ValueError):
|
||||
return False
|
||||
|
||||
|
||||
def signed_receipt(payload: dict) -> dict:
|
||||
if len(RECEIPT_KEY) < 32:
|
||||
raise RuntimeError("receipt signing key unavailable")
|
||||
body = canonical(payload)
|
||||
receipt_hash = hashlib.sha256(body).hexdigest()
|
||||
signature = hmac.new(RECEIPT_KEY.encode(), body, hashlib.sha256).hexdigest()
|
||||
return {**payload, "receipt_hash": receipt_hash, "receipt_signature": signature}
|
||||
|
||||
|
||||
def public_projection(registry: dict, human: dict, db: sqlite3.Connection | None = None) -> dict:
|
||||
projection = {
|
||||
"status": "RESOLVED",
|
||||
"canonical_id": human["human_number"],
|
||||
"subject": {
|
||||
"id": human["human_number"],
|
||||
"name": human["display_name"],
|
||||
"domain": human["responsibility_domain"],
|
||||
},
|
||||
"work_entry": {
|
||||
"domain": registry["work_entry_domain"],
|
||||
"channel": registry["work_entry_channel"],
|
||||
},
|
||||
"repository_binding": {
|
||||
"host": "guanghu.chat",
|
||||
"username": human["username"],
|
||||
"repository": human["repository"],
|
||||
"private": True,
|
||||
},
|
||||
"persona_relationships": human["personas"],
|
||||
"persona_identity_governance": registry["persona_identity_governance"],
|
||||
"registry_version": registry["version"],
|
||||
}
|
||||
if db is not None:
|
||||
relationship = db.execute(
|
||||
"SELECT decision,observed_at,receipt_hash FROM relationship_receipts WHERE human_number=? ORDER BY observed_at DESC LIMIT 1",
|
||||
(human["human_number"],),
|
||||
).fetchone()
|
||||
responsibility = db.execute(
|
||||
"SELECT decision,observed_at,receipt_hash,responsibility_version FROM responsibility_receipts WHERE human_number=? ORDER BY observed_at DESC LIMIT 1",
|
||||
(human["human_number"],),
|
||||
).fetchone()
|
||||
projection["relationship_confirmation"] = dict(relationship) if relationship else None
|
||||
projection["responsibility_receipt"] = dict(responsibility) if responsibility else None
|
||||
return projection
|
||||
|
||||
|
||||
class Handler(BaseHTTPRequestHandler):
|
||||
server_version = "GuanghuEnterpriseIdentity/1.0"
|
||||
|
||||
def log_message(self, fmt: str, *args: object) -> None:
|
||||
# Never include headers or request bodies in logs.
|
||||
print("[enterprise-identity] " + fmt % args)
|
||||
|
||||
def respond(self, status: int, body: dict) -> None:
|
||||
encoded = json.dumps(body, ensure_ascii=False).encode()
|
||||
self.send_response(status)
|
||||
self.send_header("Content-Type", "application/json; charset=utf-8")
|
||||
self.send_header("Content-Length", str(len(encoded)))
|
||||
self.send_header("Cache-Control", "no-store")
|
||||
self.send_header("X-Content-Type-Options", "nosniff")
|
||||
self.end_headers()
|
||||
self.wfile.write(encoded)
|
||||
|
||||
def body(self) -> dict:
|
||||
length = int(self.headers.get("Content-Length", "0"))
|
||||
if length < 1 or length > MAX_BODY:
|
||||
raise ValueError("request body size invalid")
|
||||
value = json.loads(self.rfile.read(length).decode())
|
||||
if not isinstance(value, dict):
|
||||
raise ValueError("JSON object required")
|
||||
return value
|
||||
|
||||
def authenticated_human(self, registry: dict, payload: dict) -> tuple[dict, str] | None:
|
||||
credentials = parse_basic(self.headers.get("Authorization", ""))
|
||||
number = str(payload.get("human_number", ""))
|
||||
human = find_human(registry, number)
|
||||
if not credentials or not human:
|
||||
return None
|
||||
username, password = credentials
|
||||
if not hmac.compare_digest(username, human["username"]) or not verify_forgejo(username, password):
|
||||
return None
|
||||
return human, username
|
||||
|
||||
def do_GET(self) -> None:
|
||||
try:
|
||||
registry = load_registry()
|
||||
except (OSError, ValueError, json.JSONDecodeError) as error:
|
||||
return self.respond(503, {"ok": False, "error": f"registry unavailable: {error}"})
|
||||
parsed = urllib.parse.urlparse(self.path)
|
||||
if parsed.path == "/health":
|
||||
return self.respond(200, {"ok": True, "service": "guanghu-enterprise-identity", "registry_version": registry["version"], "age_species": "AGE", "valid_age_individual_numbers": 0})
|
||||
if parsed.path == "/v1/resolve":
|
||||
number = urllib.parse.parse_qs(parsed.query).get("id", [""])[0]
|
||||
human = find_human(registry, number)
|
||||
return self.respond(200 if human else 404, public_projection(registry, human) if human else {"status": "NOT_FOUND"})
|
||||
return self.respond(404, {"ok": False, "error": "not found"})
|
||||
|
||||
def do_POST(self) -> None:
|
||||
try:
|
||||
registry = load_registry()
|
||||
payload = self.body()
|
||||
except (OSError, ValueError, json.JSONDecodeError) as error:
|
||||
return self.respond(400, {"ok": False, "error": str(error)})
|
||||
authenticated = self.authenticated_human(registry, payload)
|
||||
if not authenticated:
|
||||
return self.respond(401, {"ok": False, "error": "enterprise account authentication failed"})
|
||||
human, username = authenticated
|
||||
idempotency_key = str(payload.get("idempotency_key", ""))
|
||||
if not re.fullmatch(r"[A-Za-z0-9_-]{16,96}", idempotency_key):
|
||||
return self.respond(400, {"ok": False, "error": "valid idempotency_key required"})
|
||||
db = database()
|
||||
try:
|
||||
if self.path == "/v1/relationship-confirmations":
|
||||
decision = str(payload.get("decision", ""))
|
||||
if decision not in {"CONFIRM", "REJECT"}:
|
||||
return self.respond(400, {"ok": False, "error": "relationship decision invalid"})
|
||||
existing = db.execute("SELECT * FROM relationship_receipts WHERE idempotency_key=?", (idempotency_key,)).fetchone()
|
||||
if existing:
|
||||
return self.respond(200, {"ok": True, "idempotent": True, "receipt": dict(existing)})
|
||||
observed = now()
|
||||
receipt_id = "GH-REL-" + uuid.uuid4().hex.upper()
|
||||
receipt = signed_receipt({"receipt_id":receipt_id,"human_number":human["human_number"],"username":username,"registry_version":registry["version"],"decision":decision,"observed_at":observed})
|
||||
db.execute("INSERT INTO relationship_receipts VALUES (?,?,?,?,?,?,?,?,?)", (receipt_id,idempotency_key,human["human_number"],username,registry["version"],decision,observed,receipt["receipt_hash"],receipt["receipt_signature"]))
|
||||
db.execute("INSERT INTO audit(observed_at,kind,human_number_hash,receipt_id) VALUES (?,?,?,?)", (observed,"RELATIONSHIP_CONFIRMATION",hashlib.sha256(human["human_number"].encode()).hexdigest(),receipt_id))
|
||||
db.commit()
|
||||
return self.respond(201, {"ok": True, "receipt": receipt})
|
||||
if self.path == "/v1/responsibility-receipts":
|
||||
decision = str(payload.get("decision", ""))
|
||||
note = str(payload.get("note", ""))[:1000]
|
||||
version = str(payload.get("responsibility_version", ""))
|
||||
if decision not in DECISIONS or not re.fullmatch(r"[0-9]{4}-[0-9]{2}-[0-9]{2}\.[0-9]+", version):
|
||||
return self.respond(400, {"ok": False, "error": "responsibility decision or version invalid"})
|
||||
existing = db.execute("SELECT * FROM responsibility_receipts WHERE idempotency_key=?", (idempotency_key,)).fetchone()
|
||||
if existing:
|
||||
return self.respond(200, {"ok": True, "idempotent": True, "receipt": dict(existing)})
|
||||
observed = now()
|
||||
receipt_id = "GH-RESP-" + uuid.uuid4().hex.upper()
|
||||
receipt = signed_receipt({"receipt_id":receipt_id,"human_number":human["human_number"],"username":username,"responsibility_domain":human["responsibility_domain"],"responsibility_version":version,"decision":decision,"note":note,"observed_at":observed})
|
||||
db.execute("INSERT INTO responsibility_receipts VALUES (?,?,?,?,?,?,?,?,?,?,?)", (receipt_id,idempotency_key,human["human_number"],username,human["responsibility_domain"],version,decision,note,observed,receipt["receipt_hash"],receipt["receipt_signature"]))
|
||||
db.execute("INSERT INTO audit(observed_at,kind,human_number_hash,receipt_id) VALUES (?,?,?,?)", (observed,"RESPONSIBILITY_RECEIPT",hashlib.sha256(human["human_number"].encode()).hexdigest(),receipt_id))
|
||||
db.commit()
|
||||
return self.respond(201, {"ok": True, "receipt": receipt})
|
||||
if self.path == "/v1/me/entry":
|
||||
return self.respond(200, {"ok": True, "entry": public_projection(registry, human, db)})
|
||||
return self.respond(404, {"ok": False, "error": "not found"})
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
load_registry()
|
||||
if len(RECEIPT_KEY) < 32:
|
||||
raise SystemExit("GH_ENTERPRISE_RECEIPT_KEY must contain at least 32 characters")
|
||||
ThreadingHTTPServer((BIND, PORT), Handler).serve_forever()
|
||||
Loading…
Reference in a new issue